omnifaces / omnipersistence

Utilities for JPA, JDBC and DataSources
Other
31 stars 12 forks source link

Added functionality for soft deletable entities, minor fixes. #12

Closed skuntsel closed 6 years ago

skuntsel commented 6 years ago

Added functionality for soft deletable entities.

In some applications there are requirements that the entities cannot be deleted but rather inactivated, or soft deleted. To account for such requirements a new annotation is introduced, @SoftDeletable, where you specify the type of soft delete column.

Herewith two settings are proposed, SoftDeleteType.ACTIVE (where you've got an active column in the database separating active entities from soft deleted ones) and SoftDeleteType.DELETED (where you've got an deleted column in the database separating soft deleted entities from active ones). When you place the @SoftDeletable annotation on any persistent field of boolean type, the BaseEntityService will provide for helper methods to soft delete/undelete and get active entity/entities. When these methods are invoked for an entity with no fields marked as soft deleted, a NonSoftDeletableEntityException will be thrown.

Example setup:

@Entity
public class Text extends GeneratedIdEntity<Long> {

    private static final long serialVersionUID = 1L;

    @SoftDeletable(type = ACTIVE)
    private boolean active = true;

    public boolean getActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

}

and

@Stateless
public class TextService extends BaseEntityService<Long, Text> {

}

Example usage:

public void usage() {
    textService.persist(new Text());
    textService.persist(new Text());
    Text activeText = textService.getById(1L);
    textService.softDelete(activeText); // soft deletes the entity
    textService.getAllDeleted(); // returns a list of all soft deleted entities
    textService.getAllActive(); // returns a list of all active entities
    textService.getActiveById(1L); // returns null
    Text deletedText = textService.getById(1L); // returns soft deleted entity
    textService.softUnelete(activeText); // soft undeletes the entity
    textService.getActiveById(1L); // returns soft undeleted entity
}

Also, behaviour for calling BaseEntityService#save method was modified for entities which ids are not autogenerated. Now, calling this method on such entities doesn't throw IllegalEntityStateException but rather calls BaseEntityService#persist or BaseEntityService#update basing on the result of BaseEntityService#exists method call, thus there is no need to manually define which method to call.

This is now the allowed behaviour:

public void usage() {
    ManualIdEntity entity = new ManualIdEntity("code");
    manualIdEntityService.save(entity); // persists new entity to the data store
    ManualIdEntity persistedEntity = manualIdEntityService.getById("code");
    persistedEntity.setPersistentField("New value");
    manualIdEntityService.save(entity); // merges detached entity to the data store
}

Test cases for these scenarios were also created.

As a remark, for this to work correctly on the @MappedSuperclass-derived entities as well, the Reflections#accessField of the omniutils library has to be modified to account for superclass fields as well, i.e. to something like:

public static <T> T accessField(Object instance, String fieldName) {
    try {
        for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) {
            Optional<Field> field = Arrays.stream(cls.getDeclaredFields()).filter(f -> f.getName().equals(fieldName)).findFirst();
            if (field.isPresent()) {
                Field foundField = field.get();
                foundField.setAccessible(true);
                return (T) foundField.get(instance);
            }
        }
    } catch (Exception e) {
        throw new IllegalStateException(format(ERROR_ACCESS_FIELD, fieldName, instance.getClass()), e);
    }
    throw new IllegalStateException(format(ERROR_ACCESS_FIELD, fieldName, instance.getClass()));
}
BalusC commented 6 years ago

+1

Only change I'd like to see is that SoftDeleteType.DELETED is the default, not ACTIVE. It's more self-documenting and booleans themselves also default to false.

skuntsel commented 6 years ago

Yes, that's probably more convenient to set DELETED as the default type. Also, several supplementary methods might be refactored to the utility library not to mix reflection code in the service.

skuntsel commented 6 years ago

It could also make sense to add a couple of helper methods to deal with active entities in the paging section of the service class, i.e. preset additional parameter to criteria queries holding the state.

BalusC commented 6 years ago

I've as per https://github.com/omnifaces/omnipersistence/commit/a7b50b2196416e5fc55164508a11a179b8867123 inverted "Active" with "SoftDeleted" and made the default behavior of getById(), findById() and getAll() to exclude soft deleted ones.

Thank you for your contribution!