5

I have an abstract class describing a mongo document. There could be different implementations of this class that need to override an abstract method. Here is a simplified example:

@Document
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public abstract class Entity {

    @Id
    private ObjectId id;

    abstract String getSomething();
}

I want getSomething() to be written to the document as a string field. But I don't want to read it back.

I've tried to use the @AccessType annotation:

@AccessType(AccessType.Type.PROPERTY)
abstract String getSomething();

But when I'm reading this document from the db, spring throws UnsupportedOperationException: No accessor to set property. It is trying to find a setter for this field, but I don't want to define a setter for it - the method may return a calculated value and there should be no ability to change it. Although an empty setter could help, it looks more like a workaround, and I would try to avoid it.

So I'm wondering if there is a way to skip this particular property when reading from the db? Something opposite to the @Transient annotation. Or similar to @JsonIgnore in the Jackson library.

Kirill Simonov
  • 8,257
  • 3
  • 18
  • 42

3 Answers3

2

Surprisingly, there is no easy solution to make a field write only.

Although creating an empty setter would solve the problem, I feel that it breaks the least surprise principle: if you have a setter, you expect it to set a value.

So I decided to create my own @WriteOnly annotation and use it to ignore fields I don't want to read from the db:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface WriteOnly {
}

To use it you will need to extend an AbstractMongoEventListener:

@Service
public class WriteOnlyListener extends AbstractMongoEventListener<Entity> {

    @Override
    public void onAfterLoad(AfterLoadEvent<Entity> event) {
        Document doc = event.getDocument();
        if (doc == null) return;
        for (Field field: getWriteOnly(event.getType(), Class::getDeclaredFields)) {
            doc.remove(field.getName());
        }
        for (Method method: getWriteOnly(event.getType(), Class::getDeclaredMethods)) {
            doc.remove(getFieldName(method.getName()));
        }
    }

    private <T extends AccessibleObject> List<T> getWriteOnly(Class<?> type, 
                                                              Function<Class<?>, T[]> extractor) {
        List<T> list = new ArrayList<>();
        for (Class<?> c = type; c != null; c = c.getSuperclass()) {
            list.addAll(Arrays.stream(extractor.apply(c))
                    .filter(o -> o.isAnnotationPresent(WriteOnly.class))
                    .collect(Collectors.toList()));
        }
        return list;
    }

    private static String getFieldName(String methodName) {
        return Introspector.decapitalize(methodName.substring(methodName.startsWith("is") ? 2 : 
                    methodName.startsWith("get") ? 3 : 0));
    }

}
Kirill Simonov
  • 8,257
  • 3
  • 18
  • 42
1

You may try adding @Transient property so that it is ignored while mapping fields. Refer to this question.

On the other hand if the property is just to be ignored while reading then you may specify an empty setter method. This will avoid the exception of no accessor method.

Or create a custom annotation and put all the properties for which to have empty setter method in a separate class and annotate the class with new annotation. This class then can be extended where required. You may look at lombok project to see how they that created annotations for getter and setter methods.

Ravik
  • 694
  • 7
  • 12
  • 1
    Creating an empty setter could help, but it looks more like a workaround and will be a little confusing. I would like to avoid it if possible: *It is trying to find a setter for this field, but I don't want to define a setter for it.* – Kirill Simonov Oct 21 '19 at 16:02
1

The problem in your case is the name of the method that has the word "get".

By having the word "get", Spring thinks there is an attribute called "something", and with that it checks if it has its respective getter and setter method.

Rename your method from "getSomething" to, for example, "calculatedInformation".

Haruo
  • 516
  • 2
  • 4
  • 1
    If I rename the method to have no `get`, spring will not persist its result. It looks like `@AccessType(AccessType.Type.PROPERTY)` only works for getters and setters – Kirill Simonov Oct 25 '19 at 18:51
  • You said "the method may return a calculated value and there should be no ability to change it". So you just want to persist this calculated information once and then just read this information? – Haruo Oct 29 '19 at 14:11
  • I want to write the return value of this method to the db, but not read it back. I'm fine with renaming it, but it looks like in this case spring just ignores it from both write and read. Maybe `@AccessType` is not the right approach – Kirill Simonov Oct 29 '19 at 14:20