Использование дженериков в хранилищах данных 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);
}
}
Мне пришло в голову, что может быть возможно заменить несколько классов для каждого типа объектов тремя классами, основанными на генериках, таким образом сохраняя много кодировки. Я не совсем уверен, как это сделать, и на самом деле, если это хорошая идея?
Ответы
Ответ 1
Прежде всего, я знаю, что мы немного поднимаем планку, но это уже намного меньше кода, чем вы должны были писать без помощи 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 более высокого уровня, является гораздо более подходящим вариантом и обычно организует вызовы в несколько репозиториев.
Кроме того, если вы строите сервисы поверх репозиториев, вы обычно хотите заставить клиентов использовать сервис вместо репозитория (классический пример здесь заключается в том, что сервис для управления пользователями также запускает генерацию пароля и шифрование, так что ни в коем случае было бы неплохо позволить разработчикам использовать репозиторий напрямую, поскольку они будут эффективно обходить шифрование). Таким образом, вы обычно хотите быть избирательными в отношении того, кто может сохранять объекты домена, чтобы не создавать зависимости повсеместно.
Резюме
Да, вы можете создавать общие репозитории и использовать их с несколькими типами доменов, но есть довольно строгие технические ограничения. Тем не менее, с архитектурной точки зрения сценарий, который вы описали выше, даже не должен появляться, так как это означает, что вы все равно сталкиваетесь с запахом дизайна.
Ответ 2
Я работаю над проектом по созданию общего репозитория для Кассандры с данными весны.
Сначала создайте интерфейс репозитория с кодом.
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, чтобы найти способ инициализации интерфейса репозитория spring.
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 с аналогичной логикой.
Ответ 3
Я просто работаю над родовым хранилищем данных, генерируя интерфейс и компилируя его во время выполнения. Пожалуйста, найдите репозиторий ниже для реализации и пример кода.
https://github.com/maye-msft/generic-repository-springdata