Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring mongo write only field

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.

like image 504
Kirill Simonov Avatar asked Oct 21 '19 03:10

Kirill Simonov


3 Answers

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));
    }

}
like image 79
Kirill Simonov Avatar answered Oct 22 '22 00:10

Kirill Simonov


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.

like image 31
Ravik Avatar answered Oct 22 '22 00:10

Ravik


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".

like image 21
Haruo Avatar answered Oct 21 '22 23:10

Haruo