I'm developing a multi-module CMS application following Domain-Driven Design principles. I'm trying to figure out how to implement Generic Repository, thus avoiding a lot of boiler-plate code.
The idea is to have a "two-way" mapping strategy (model to entity and vice versa) and Generic Repository implemented in the Persistence module. Further, an interface in the Domain module would act as a contract between Domain and Persistence, so I can use it for later injection in the other layers.
How can I make this interface generic?
To be specific, the problem here is the mapping. Since I'm using a "two-way" mapping strategy, the Domain module has no idea about DB specific entities.
Is there a way to map generic type models between layers? Or use some other mapping strategy while keeping the layers loosely coupled?
Here is a code example to clarify what I'm trying to achieve. This would be the code example for Generic Repository:
@MappedSuperclass
public abstract class AbstractJpaMappedType {
…
String attribute
}
@Entity
public class ConcreteJpaType extends AbstractJpaMappedType { … }
@NoRepositoryBean
public interface JpaMappedTypeRepository<T extends AbstractJpaMappedType>
extends Repository<T, Long> {
@Query("select t from #{#entityName} t where t.attribute = ?1")
List<T> findAllByAttribute(String attribute);
}
public interface ConcreteRepository
extends JpaMappedTypeRepository<ConcreteType> { … }
Further, I want to make my own Custom Repository to be able to do some mapping of model to entity and vice versa, so I wouldn't have JPA specific annotation in my domain classes, thus making it loosely coupled. I want this Custom Repository to implement an interface from Domain module, allowing me to inject it later in the Services layer.
public class CustomRepositoryImpl implements CustomRepository {
public final JpaMappedTypeRepository<T> repository;
...
}
How can I make this class and this interface generic so that I would be able to do mapping between model and entity, since Domain layer has no information about entity classes?
I figured it out eventually.
The problem, as it was stated in the question, was the mapping between layers. I created a mapping interface to declare mapping methods. I used @ObjectFactory
annotation from MapStruct to deal with generic mapping (look here):
public interface EntityMapper<M, E> {
M toModel(E entity);
List<M> toModelList(List<E> entities);
E toEntity(M model);
List<E> toEntityList(List<M> models);
// default object factory methods
}
Then I proceeded with creating a mapper for each of the child classes and extending it with the EntityMapper interface with concrete types that I want to map.
@Mapper(componentModel="spring")
public interface ConcreteEntityMapper extends EntityMapper<ConcreteModelType, ConcreteJpaType> {
}
I created an abstract class where I injected the JPA repository and the mapper, and also implemented common methods.
abstract class CustomRepositoryImpl<T extends AbstractModelMappedType, E extends AbstractJpaMappedType> {
private final JpaMappedTypeRepository<E> repository;
private final EntityMapper<M, E> mapper;
//... common methods for mapping and querying repositories.
}
Then I extended a ConcreteTypeRepositoryImpl with this abstract class, and implemented a generic interface, which I can later use as a reference in other layers.
public interface CustomRepository<M> {
M saveOrUpdate(M model);
Optional<M> getById(Long id);
List<M> getByName(String name);
List<M> getAll();
void delete(Long id);
}
@Component
public class ConcreteTypeRepositoryImpl extends CustomRepositoryImpl<ConcreteModelType,ConcreteJpaType> implements CustomRepository<ConcreteModelType> {
public ConcreteTypeRepositoryImpl(JpaMappedTypeRepository<ConcreteJpaType> repository,
EntityMapper<ConcreteModelType, ConcreteJpaType> mapper) {
super(repository, mapper);
}
}
And that would be it. Now I can inject CustomRepository into other layers and hit desired repository.