Статьи

Использование CDI / Weld для внедрения JPA / Hibernate Entity Manager

В этом посте мы узнаем, как легко использовать возможности CDI (внедрение контекстов и зависимостей) / Weld при разработке полнофункционального приложения JPA / Hibernate . По своей природе разрешающая природа CDI позволяет буквально превратить любой класс в инъецируемый компонент, что делает внедрение ресурсов Java EE таким же простым, как внедрение POJO . Это означает, что, например, менеджеры сущностей и даже объекты источников данных могут быть внедрены где угодно, используя всего несколько пользовательских квалификаторов.

Это очень мощная, но в то же время довольно недооцененная функция, которая часто упускается из виду новичками CDI, которые просто не знают, что стандарт предоставляет эту функциональность «из коробки». Давайте теперь посмотрим, как сделать менеджер сущностей инъекционным ресурсом и создать простое приложение JPA с нуля!

В недавней статье я рассмотрел основы тандема CDI / Weld, начиная от использования @Default , @Alternative и @Produces и @Produces работой с несколькими более @Produces функциями, такими как полиморфные точки инъекции и пользовательские квалификаторы. Я использовал их для разработки наивного автономного приложения, которое показывало, как во время выполнения вводить разные реализации простого интерфейса в объект клиента для анализа строки, введенной в консоли. Мы будем использовать эти навыки сейчас, поэтому, если вы не совсем уверены, как это делать, читайте об этом — я подожду.

Учитывая, что в этом пошаговом руководстве мы будем интенсивно работать с JPA / Hibernate, я предполагаю, что у вас есть хотя бы минимальный опыт работы с ними — в противном случае проверьте это руководство по Hibernate .

Создание Injectable Entity Manager

Суть любого приложения JPA на основе CDI заключается в создании менеджера сущностей для инъекций. Стоит отметить, что независимо от подхода, который мы используем для получения менеджера, когда он становится инъецируемым ресурсом Java EE, полные примеры, показанные в оставшейся части поста, одинаково верны. С этого момента создание и внедрение всех видов объектов в разные слои довольно просто.

Итак, первая задача, которую нам нужно решить, — это именно то, что делает менеджера сущностей инъецируемым ресурсом. При этом мы можем связать JPA с Hibernate (эталонная реализация JPA) и без проблем запускать операции CRUD на некоторых наивных объектах JPA.

На самом деле, есть несколько простых подходов, которые мы можем выбрать для достижения этой цели.

Использование аннотации @PersistentContext

Наиболее безболезненным способом, который работает только с сервером приложений Java EE , таким как Apache TomEE , JBoss Widlfly или Jetty , является аннотация @PersistenceContext . Как и следовало ожидать, эта методология связывает контекст персистентности (и, конечно, блок персистентности) с менеджером сущностей, а жизненный цикл менеджера полностью управляется контейнером (он же диспетчер сущностей, управляемый контейнером).

На широком общем уровне этот подход может быть реализован с помощью специального классификатора, подобного следующему:

 @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface MySQLDatabase {} public class EntityManagerProducer { @Produces @PersistenceContext(unitName = "my-persistence-unit") @MySQLDatabase private EntityManager entityManager; } 

В этом случае атрибут unitName указывает имя выборочной единицы постоянства, связанной с соответствующим контекстом постоянства. Поэтому, если вы собираетесь использовать этот подход, убедитесь, что имя соответствует имени, указанному в файле persistence.xml в Hibernate.

Имея этот код, простой класс DAO, который принимает диспетчер сущностей в конструкторе, можно определить следующим образом:

 public class MyDao { private EntityManager entityManager; @Inject public MyDao(@MySQLDatabase EntityManager entityManager) { this.entityManager = entityManager; } } 

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

Использование метода продюсера

Просто Java, CDI и Weld должны выполнить работу за нас. Как мы делаем инъекцию менеджера в этой среде? Что ж, мы можем инкапсулировать создание менеджера неуправляемых сущностей внутри метода продюсера и привязать к нему пользовательский квалификатор следующим образом:

 @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface MySQLDatabase {} public class EntityManagerProducer { @Produces @MySQLDatabase public EntityManager createEntityManager() { return Persistence .createEntityManagerFactory("my-persistence-unit") .createEntityManager(); } public void close( @Disposes @MySQLDatabase EntityManager entityManager) { entityManager.close(); } } 

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

Это JPA на самом элементарном уровне, поэтому единственное, на что стоит обратить внимание, это использование аннотации @Disposes . Он сообщает контейнеру CDI, что этот метод закрывает диспетчер сущностей, что заставляет контейнер вызывать его перед освобождением менеджера.

На этом этапе нам удалось создать полностью внедряемый менеджер сущностей с простым методом продюсера. Но давайте подумаем над этим: почему мы должны беспокоиться о поворотах этого процесса, если мы не собираемся использовать менеджера продуктивно? Хлоп!

Как мы видели ранее, типичный вариант использования — это внедрение менеджера в класс DAO, что позволило бы нам выполнять операции CRUD над некоторыми объектами JPA через легко потребляемый API и, что самое главное, без необходимости излишней экспозиции из сверху вниз API менеджера к клиентскому коду. Наиболее эффективный способ разработки этого абстрактного API-интерфейса — использование простого уровня доступа к данным (DAL). CDI делает создание этого слоя быстрым, но, как обычно, пример поможет нам намного легче понять внутреннюю работу этого процесса.

Абстрагирование доступа к данным с помощью базового уровня доступа к данным (DAL)

Хотя, конечно, можно запускать операции CRUD на объектах JPA, используя менеджер объектов в качестве отдельного компонента, при этом мы упустили бы преимущества трех больших столпов объектно-ориентированного проектирования. Первый — это внедрение самих зависимостей (именно поэтому в CDI есть DI), второй — разделение интересов, третий — абстракция.

Уровень доступа к данным — это дополнительный уровень, который осуществляет доступ к базовому механизму хранения (в данном случае это база данных MySQL, но это может быть что угодно). Реализуя хотя бы базовый DAL, мы убили бы двух зайцев одним выстрелом: во-первых, мы использовали бы CDI для внедрения менеджера сущностей в этот уровень, а во-вторых, мы бы представили аккуратный абстрактный интерфейс для доступа к рассматриваемому хранилищу к клиентскому коду.

В этом случае уровень будет состоять из универсального интерфейса и только одной реализации (даже при том, что еще несколько могут быть добавлены в будущем для замены механизмов сохранения, например, других, чем объектно-реляционная база данных, во время выполнения):

 public interface EntityDao<T> { T find(int id); List<T> findAll(String table); void update(int id, Consumer<T>... updates) throws Exception; void save(T entity); void remove(int id); } 

Приведенный выше интерфейс моделирует типичный API-интерфейс CRUD, и разработчики, соблюдающие его, смогут запускать операции CRUD для заданного объекта типа T.

Реализатор, с другой стороны, немного сложнее, но он в двух словах показывает, как внедрить диспетчер неуправляемых объектов в конструктор класса DAO:

 public class BaseEntityDao<T> implements EntityDao<T> { private EntityManager entityManager; private Class<T> entityClass; @Inject public BaseEntityDao( @MySQLDatabase EntityManager entityManager, @UserClass Class<T> entityClass) { this.entityManager = entityManager; this.entityClass = entityClass; } public T find(int id) { return entityManager.find(entityClass, id); } public List<T> findAll(String table) { String str = "select e from table e"; String jpql = str.replace("table", table); return entityManager.createQuery(jpql).getResultList(); } public void update(int id, Consumer<T>... updates) throws Exception { T entity = find(id); Arrays.stream(updates).forEach(up -> up.accept(entity)); beginTransaction(); commitTransaction(); } public void save(T entity) { beginTransaction(); entityManager.persist(entity); commitTransaction(); } public void remove(int id) { T entity = find(id); beginTransaction(); entityManager.remove(entity); commitTransaction(); } private void beginTransaction() { try { entityManager.getTransaction().begin(); } catch (IllegalStateException e) { rollBackTransaction(); } } private void commitTransaction() { try { entityManager.getTransaction().commit(); } catch (IllegalStateException | RollbackException e) { rollBackTransaction(); } } private void rollBackTransaction() { try { entityManager.getTransaction().rollback(); } catch (IllegalStateException | PersistenceException e) { e.printStackTrace(); } } } 

(Примечание: я хотел бы отдать должное нашему редактору Java Channel Николаю Парлогу , который после некоторой продуктивной обратной связи с GitHub предоставил более надежную реализацию метода update() чем тот, который я написал изначально) .

Созданный на основе классического отношения агрегации , класс BaseEntityDao представляет собой просто адаптер с декларативным API, который выполняет операции CRUD с предоставленной сущностью. Но фактическая рабочая лошадка класса (она же адаптив), которая продвигает всю эту функциональность за API, — это внедренный менеджер сущностей! Видите, как легко внедрить менеджера в класс DAO с помощью CDI? Готов поспорить что ты.

Более того, этот класс предоставляет клиентскому коду только методы CRUD, при этом аккуратно скрывая весь API менеджера сущностей, так как он не требуется в этом конкретном домене.

Кроме того, BaseEntityDao объявляет зависимость от класса домена, который моделирует сущности пользователя. Вот пользовательский квалификатор и производитель, необходимые для введения этого класса:

 @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER}) public @interface UserClass {} public class UserClassProducer { @Produces @UserClass public Class createUserClass() { return User.class; } } 

Класс UserClassProducer является типичным производителем CDI, который создает, да … класс сущности User . Для краткости я не включил здесь класс сущности, потому что он выглядит точно так же, как и в предыдущем посте Hibernate . Не стесняйтесь проверить это там.

На данный момент мы реализовали функциональный уровень доступа к данным, который использует класс DAO для выполнения операций CRUD на некоторых пользовательских объектах. Хотя это довольно удобно, так как класс может эффективно использоваться в различных контекстах и ​​сценариях, здесь не хватает крошечной детали.

Хотя это и не является строго обязательным, было бы неплохо реализовать хотя бы тонкий прикладной уровень и исключить всю функциональность, необходимую для того, чтобы пользователи могли запускать операции CRUD через уровень доступа к данным. Это сделало бы еще более понятным, как использовать CDI / Weld в качестве основного механизма DI в приложении JPA.

Простой прикладной уровень

Существует действительно широкий спектр сред, в которых можно использовать наш аккуратный уровень доступа к данным, начиная от веб-приложений и мобильных приложений и заканчивая настольными приложениями. Варианты в значительной степени безграничны. Однако для простоты мы будем использовать его для извлечения, обновления и удаления пользовательских сущностей из базы данных MySQL на основе набора числовых опций, отображаемых в консоли.

Вот как может быть реализован простой прикладной уровень, функциональность которого сводится к выполнению определенной операции CRUD над пользовательским объектом в соответствии со строкой, введенной в консоли. Конечно, зависимость, которая фактически выполняет эти операции, является экземпляром класса BaseEntityDao , который внедряется в конструктор вместе с объектом BufferedReader . Этот соавтор создается методом продюсера, как показано во вводной статье CDI .

 public class UserApplication { private EntityDao<User> userDao; private BufferedReader userInputReader; @Inject public UserApplication( BaseEntityDao<User> userDao, BufferedReader userInputReader) { this.userDao = userDao; this.userInputReader = userInputReader; } public void run() throws Exception { // runs until the user quits Boolean running = true; while (running) { System.out.println("Enter an option: " + "1) Insert a new user. " + "2) Find a user. " + "3) List all users " + "4) Edit a user. " + "5) Delete a user. " + "6) Quit the application"); running = runUserOperation(readUserInput()); } } private boolean runUserOperation(String option) throws Exception { switch (option) { case "1": persistNewUser(); return true; case "2": fetchExistingUser(); return true; case "3": fetchAllExistingUsers(); return true; case "4": updateExistingUser(); return true; case "5": removeExistingUser(); return true; case "6": return false; } return true; } private void persistNewUser() throws IOException { String name = requestStringInput("the name of the user"); String email = requestStringInput("the email of the user"); userDao.save(new User(name, email)); } private void fetchExistingUser() throws IOException { int id = requestIntegerInput("the user ID"); User user = userDao.find(id); System.out.println(user); } private void fetchAllExistingUsers() throws IOException { userDao.findAll("User").stream().forEach(System.out::println); } private void updateExistingUser() throws Exception { int id = requestIntegerInput("the user ID"); String name = requestStringInput("the name of the user"); String email = requestStringInput("the email of the user"); userDao.update(id, user -> user.setName(name), user -> user.setEmail(email)); } private void removeExistingUser() throws IOException { int id = requestIntegerInput("the user ID"); userDao.remove(id); } private String readUserInput() throws IOException { return userInputReader.readLine(); } private String requestStringInput(String request) throws IOException { System.out.printf("Enter %s: ", request); return readUserInput(); } private int requestIntegerInput(String request) throws IOException { System.out.printf("Enter %s: ", request); return Integer.parseInt(readUserInput()); } } 

И последнее, но не менее важное: мы должны запустить приложение из нашей IDE и посмотреть, действительно ли оно выполняет то, что обещает. Для этого нам нужно программно загрузить Weld и получить экземпляр класса UserApplication из контейнера Weld, как UserApplication ниже.

 public class Main { public static void main(String[] args) throws IOException { Weld weld = new Weld(); WeldContainer container = weld.initialize(); UserApplication userApplication = container.instance() .select(UserApplication.class) .get(); userApplication.run(); weld.shutdown(); } } 

Если все идет хорошо, нам нужно ввести числовую опцию в консоли. Это приведет к вставке, извлечению, обновлению или удалению пользовательского объекта из соответствующей базы данных MySQL (кроме случаев, когда мы введем опцию 6, поскольку это просто приведет к выходу из программы).

Помимо ограниченной, довольно наивной функциональности этого примера, он полезен для демонстрации того, как использовать CDI / Weld в реализации полнофункционального приложения JPA, и насколько мощным является стандарт, когда речь идет о внедрении зависимостей без усилий. путь. Таким образом, независимо от уровня сложности, который вы хотите внедрить в зависимости ваших классов, будь то POJO, менеджеры сущностей, объекты источника данных, назовите его: CDI создаст ваши графы объектов для вас за кулисами, и лучше всего всего лишь с несколькими минимальными требованиями.

Резюме

Тандем CDI / Weld упрощает создание приложений JPA, так как стандарт облегчает создание менеджеров объектов для инъекций либо с помощью аннотации @PersistentContext , либо с помощью комбинации методов производителя и пользовательских квалификаторов.

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

Но наличие такого утопического мышления вдали от прагматизма не дает нам ничего действительно полезного. Мы все знаем, что CDI (и каждая имеющаяся там среда ORM) активно использует Reflection API для своей работы, а также набор стандартизированных аннотаций.

Если вы не против жить с этим, тогда CDI сделает вашу жизнь намного проще. С другой стороны, если вы более ортодоксальны и чувствуете себя более комфортно, используя простые фабрики старой школы и конструкторы для построения ваших графов объектов, ну, это тоже хорошо.