Использование дженериков в репозиториях Spring Data JPA

У меня есть несколько простых типов объектов, которые необходимо сохранить в базе данных. Я использую Spring JPA для управления этим постоянством. Для каждого типа объекта мне нужно построить следующее:

import org.springframework.data.jpa.repository.JpaRepository;

public interface FacilityRepository extends JpaRepository<Facility, Long> {
}


public interface FacilityService {
    public Facility create(Facility facility);
}

@Service
public class FacilityServiceImpl implements FacilityService {

    @Resource
    private FacilityRepository countryRepository;

    @Transactional
    public Facility create(Facility facility) {
        Facility created = facility;
        return facilityRepository.save(created);
    }
}

Мне пришло в голову, что может быть возможно заменить несколько классов для каждого типа объекта тремя классами, основанными на обобщенных классах, тем самым сэкономив много стандартного кодирования. Я не совсем уверен, как это сделать и на самом деле, если это хорошая идея?

4 ответа

Решение

Прежде всего, я знаю, что мы немного поднимаем планку, но это уже намного меньше кода, чем нужно было писать без помощи Spring Data JPA.

Во-вторых, я думаю, что вам, во-первых, не нужен класс обслуживания, если все, что вам нужно сделать, это переслать вызов в хранилище. Мы рекомендуем использовать службы перед репозиториями, если у вас есть бизнес-логика, которая требует согласования различных репозиториев в транзакции или имеет иную бизнес-логику для инкапсуляции.

Вообще говоря, вы можете сделать что-то вроде этого:

interface ProductRepository<T extends Product> extends CrudRepository<T, Long> {

    @Query("select p from #{#entityName} p where ?1 member of p.categories")
    Iterable<T> findByCategory(String category);

    Iterable<T> findByName(String name);
}

Это позволит вам использовать репозиторий на стороне клиента следующим образом:

class MyClient {

  @Autowired
  public MyClient(ProductRepository<Car> carRepository, 
                  ProductRepository<Wine> wineRepository) { … }
}

и это будет работать как положено. Однако есть несколько вещей, на которые стоит обратить внимание:

Это работает только в том случае, если доменные классы используют наследование одной таблицы. Единственная информация о классе домена, которую мы можем получить во время начальной загрузки, это то, что он будет Product объекты. Так что для таких методов, как findAll() и даже findByName(…) соответствующие запросы начнутся с select p from Product p where…, Это связано с тем, что поиск отражений никогда не сможет произвести Wine или же Car если вы не создадите специальный интерфейс репозитория для него, чтобы захватить конкретную информацию типа.

Вообще говоря, мы рекомендуем создавать интерфейсы репозитория для совокупного корня. Это означает, что у вас нет репо для каждого класса домена как такового. Еще важнее то, что абстракция службы 1:1 над хранилищем также полностью упускает смысл. Если вы создаете сервисы, вы не создаете один для каждого репозитория (обезьяна может сделать это, а мы не обезьяны, не так ли?). Служба предоставляет API более высокого уровня, является гораздо более подходящим вариантом и обычно организует вызовы в несколько репозиториев.

Кроме того, если вы строите сервисы поверх репозиториев, вы обычно хотите заставить клиентов использовать сервис вместо репозитория (классический пример здесь заключается в том, что сервис для управления пользователями также запускает генерацию пароля и шифрование, так что ни в коем случае было бы неплохо позволить разработчикам использовать репозиторий напрямую, поскольку они будут эффективно обходить шифрование). Таким образом, вы обычно хотите быть избирательными в отношении того, кто может сохранять объекты домена, чтобы не создавать зависимости повсеместно.

Резюме

Да, вы можете создавать общие репозитории и использовать их с несколькими типами доменов, но есть довольно строгие технические ограничения. Тем не менее, с архитектурной точки зрения сценарий, который вы описали выше, должен даже появиться, поскольку это означает, что вы все равно столкнетесь с запахом дизайна.

Это вполне возможно! Я, наверное, очень опаздываю на вечеринку. Но это обязательно кому-то поможет в будущем. Вот полное решение, которое работает как шарм!

Создайте BaseEntity class для ваших сущностей следующим образом:

@MappedSuperclass
public class AbstractBaseEntity implements Serializable{

    @Id @GeneratedValue
    private Long id;
    @Version
    private int version;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    public AbstractBaseEntity() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    // getters and setters      
}

Создайте общий интерфейс репозитория JPA для сохранения DAO следующим образом: NB. Не забудьте поставить@NoRepositoryBean так что JPA не будет пытаться найти реализацию для репозитория!

@NoRepositoryBean
public interface AbstractBaseRepository<T extends AbstractBaseEntity, ID extends Serializable>
extends JpaRepository<T, ID>{
    
}

Создайте базовый класс службы, который использует указанный выше базовый репозиторий JPA. Это тот, который другие сервисные интерфейсы в вашем домене будут просто расширять следующим образом:

public interface AbstractBaseService<T extends AbstractBaseEntity, ID extends Serializable>{
    public abstract T save(T entity);
    public abstract List<T> findAll(); // you might want a generic Collection if u prefer

    public abstract Optional<T> findById(ID entityId);
    public abstract T update(T entity);
    public abstract T updateById(T entity, ID entityId);   
    public abstract void delete(T entity);
    public abstract void deleteById(ID entityId);
    // other methods u might need to be generic
    
}

Затем создайте абстрактную реализацию для базового репозитория JPA, и базовым методам CRUD также будут предоставлены их реализации, как показано ниже:

@Service
@Transactional
public abstract class AbstractBaseRepositoryImpl<T extends AbstractBaseEntity, ID extends Serializable>
        implements AbstractBaseService<T, ID>{
    
    private AbstractBaseRepository<T, ID> abstractBaseRepository;
    
    @Autowired
    public AbstractBaseRepositoryImpl(AbstractBaseRepository<T, ID> abstractBaseRepository) {
        this.abstractBaseRepository = abstractBaseRepository;
    }
    
    @Override
    public T save(T entity) {
        return (T) abstractBaseRepository.save(entity);
    }

    @Override
    public List<T> findAll() {
        return abstractBaseRepository.findAll();
    }

    @Override
    public Optional<T> findById(ID entityId) {
        return abstractBaseRepository.findById(entityId);
    }

    @Override
    public T update(T entity) {
        return (T) abstractBaseRepository.save(entity);
    }

    @Override
    public T updateById(T entity, ID entityId) {
        Optional<T> optional = abstractBaseRepository.findById(entityId);
        if(optional.isPresent()){
            return (T) abstractBaseRepository.save(entity);
        }else{
            return null;
        }
    }

    @Override
    public void delete(T entity) {
        abstractBaseRepository.delete(entity);
    }

    @Override
    public void deleteById(ID entityId) {
        abstractBaseRepository.deleteById(entityId);
    }

}

Как использовать приведенный выше аннотация entity, service, repository, а также implementation:

Примером здесь будет MyDomain организация

  1. Создайте объект домена, который расширяет AbstractBaseEntity следующим образом: NB. ID, createdAt, updatedAt, versionи т. д. будут автоматически включены в MyDomain сущность из AbstractBaseEntity

    Открытый класс @Entity MyDomain расширяет AbstractBaseEntity{

     private String attribute1;
     private String attribute2;
     // getters and setters
    

    }

Затем создайте repository для MyDomain сущность, которая расширяет AbstractBaseRepository следующим образом:

@Repository
public interface MyDomainRepository extends AbstractBaseRepository<MyDomain, Long>{

}

Кроме того, создайте service интерфейс для MyDomain субъект следующим образом:

public interface MyDomainService extends AbstractBaseService<MyDomain, Long>{

}

Затем предоставьте реализацию для MyDomain сущность, которая расширяет AbstractBaseRepositoryImpl реализация следующим образом:

@Service
@Transactional
public class MyDomainServiceImpl extends AbstractBaseRepositoryImpl<MyDomain, Long> 
        implements MyDomainService{
    private MyDomainRepository myDomainRepository;

    public MyDomainServiceImpl(MyDomainRepository myDomainRepository) {
        super(myDomainRepository);
    }
    // other specialized methods from the MyDomainService interface

}
Now use your `MyDomainService` service in your controller as follows: 

@RestController // or @Controller
@CrossOrigin
@RequestMapping(value = "/")
public class MyDomainController {
    
    private final MyDomainService myDomainService;

    @Autowired
    public MyDomainController(MyDomainService myDomainService) {
        this.myDomainService = myDomainService;
    }
   
    @GetMapping
    public List<MyDomain> getMyDomains(){
        return myDomainService.findAll();
    }   
    // other controller methods

}

NB. Убедитесь, чтоAbstractBaseRepository аннотируется @NoRepositoryBean так что JPAне пытается найти реализацию для bean-компонента. Так жеAbstractBaseServiceImpl должен быть помечен как абстрактный, иначе JPA попытается автоматически подключить все дочерние daos AbstractBaseRepository в конструкторе класса, ведущего к NoUniqueBeanDefinitionExceptionпоскольку при создании bean-компонента будет введено более 1 daos (репозитория)! Теперь твойservice, repository, а также implementationsболее многоразовые. Мы все ненавидим шаблон!

Надеюсь, это кому-то поможет.

Я нашел еще один способ сделать это, используя на один класс меньше, вдохновленный ответом @Jose Mhlanga.

СоздатьBaseEntity

      @Getter
@Setter
@MappedSuperclass
public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
}

СоздатьBaseRepository

      @NoRepositoryBean
public interface BaseRepository<T extends BaseEntity> extends JpaRepository<T, Long> { }

Наконец создайтеBaseServiceчтобы завершить настройку

      public interface BaseService<T extends BaseEntity> {

    BaseRepository<T> getRepository();

    default T create(T t) {
        return getRepository().save(t);
    }
    
    default Optional<T> update(T t) {
        if (getRepository().existsById(t.getId())) {
            return Optional.of(getRepository().save(t));
        }
        return Optional.empty();
    }
    
    default Optional<T> get(Long id) {
        return getRepository().findById(id);
    }
    
    default List<T> getAll() {
        return getRepository().findAll();
    }
    
    default void delete(Long id) {
        getRepository().deleteById(id);
    }
    
}

Теперь мы можем приступить к созданию наших объектов. Допустим, у нас есть объект с именемCategory. Создадим модель, репозиторий, сервис и контроллер.

Сущность и репозиторий будут выглядеть примерно так:

      @Getter
@Setter
@Entity
public class Category extends BaseEntity { String name; }

public interface CategoryRepository extends BaseRepository<Category> { }

Для обслуживания нам нужно переопределить только один метод:getRepository()

      @ApplicationScoped
public class CategoryService implements BaseService<Category> {

    @Inject
    CategoryRepository categoryRepository;

    @Override
    public BaseRepository<Category> getRepository() {
        return categoryRepository;
    }

}

Наконец, мы создаем контроллер. Я еще не совсем понял, где я мог бы создать абстракцию для контроллера. Отредактирую это, если сделаю.

      @Path("categories")
@ApplicationScoped
public class CategoryController {

    // You can (and should!) use BaseService here.
    // Omitting that part as that would need qualifier.
    @Inject
    CategoryService categoryService;

    @POST
    public Response create(Category category) {
        return Response.status(Status.CREATED).entity(categoryService.create(category)).build();
    }

    @PUT
    @Path("{id}")
    public Response update(Category category, @PathParam("id") Long id) {
        if (Objects.isNull(category.getId()))
            category.setId(id);
        return categoryService.update(category).map(i -> Response.status(Status.ACCEPTED).entity(i).build())
                .orElseGet(() -> Response.status(Status.NOT_FOUND).build());
    }
    
    @DELETE
    @Path("{id}")
    public Response delete(@PathParam("id") Long id) {
        categoryService.delete(id);
        return Response.status(Status.ACCEPTED).build();
    }
    
    @GET
    @Path("{id}")
    public Response get(@PathParam("id") Long id) {
        return categoryService.get(id).map(i -> Response.status(Status.OK).entity(i).build())
                .orElseGet(() -> Response.status(Status.NO_CONTENT).build());
    }
    
    @GET
    public Response get() {
        return Response.status(Status.OK).entity(categoryService.getAll()).build();
    }
}

Надеюсь это поможет. Ваше здоровье!

Я работаю над проектом по созданию общего репозитория для Кассандры с данными весны.

Сначала создайте интерфейс репозитория с кодом.

StringBuilder sourceCode = new StringBuilder();
sourceCode.append("import org.springframework.boot.autoconfigure.security.SecurityProperties.User;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.AllowFiltering;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.Query;\n");
sourceCode.append("import org.springframework.data.repository.CrudRepository;\n");
sourceCode.append("\n");
sourceCode.append("public interface TestRepository extends CrudRepository<Entity, Long> {\n");
sourceCode.append("}");

Скомпилируйте код и получите класс, я использую org.mdkt.compiler.InMemoryJavaCompiler

ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
compiler = InMemoryJavaCompiler.newInstance();
compiler.useParentClassLoader(classLoader);
Class<?> testRepository = compiler.compile("TestRepository", sourceCode.toString());

И инициализировать репозиторий в весенний период выполнения данных. Это немного сложно, так как я отлаживаю код SpringData, чтобы найти способ инициализации интерфейса репозитория весной.

CassandraSessionFactoryBean bean = context.getBean(CassandraSessionFactoryBean.class);
RepositoryFragments repositoryFragmentsToUse = (RepositoryFragments) Optional.empty().orElseGet(RepositoryFragments::empty); 
CassandraRepositoryFactory factory = new CassandraRepositoryFactory(
    new CassandraAdminTemplate(bean.getObject(), bean.getConverter()));
factory.setBeanClassLoader(compiler.getClassloader());
Object repository = factory.getRepository(testRepository, repositoryFragmentsToUse);

Теперь вы можете попробовать метод сохранения репозитория и другие методы, такие как findById.

Method method = repository.getClass().getMethod("save", paramTypes);
T obj = (T) method.invoke(repository, params.toArray());

Полный пример кода и реализации я поместил в этот репозиторий https://github.com/maye-msft/generic-repository-springdata.

Вы можете расширить его до JPA с аналогичной логикой.

Другие вопросы по тегам