Search code examples
javaspring-bootspring-data-jpadomain-driven-designddd-repositories

Generic Repository within DDD: How can I make this interface generic?


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?


Solution

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