Использование дженериков в репозиториях 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
организация
Создайте объект домена, который расширяет
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 с аналогичной логикой.