Статьи

Переключение источника данных с альтернативами CDI и стереотипами

Здесь мы используем H2 в нашей тестовой среде, Derby в разработке и Postgres в производстве. Это 2014 год, и у Java EE все еще нет приличной  спецификации конфигурацииТак как же мы изменим источники данных в Java EE в зависимости от нашей среды?  Есть несколько возможностей (из внешних файлов свойств, фильтрации ресурсов Maven, профилей Maven с другой конфигурацией или трюков JNDI), но этот пост будет сосредоточен только на одном:  переключение источника данных с альтернативами и стереотипами CDI .

Случай использования

Допустим, у нас есть веб-приложение Java EE (приложение CRUD для   объекта Speaker, использующего JSF, EJB, JPA и CDI), работающее на JBoss, и нам нужно легко переключать источники данных: источник данных  H2  в памяти в нашей тестовой среде,  Derby  DataSource для развития и  Postgres  DataSource в производстве. Каждый источник данных имеет уникальное имя JNDI и указывает на базу данных (в памяти или на сервере). Затем, с небольшой настройкой XML (да, Java EE по-прежнему использует XML, а не JSon; o),  мы хотим переключить источники данных . Вот как это выглядит на хорошей диаграмме:

Переключите источник данных с альтернативами CDI DS

Обратите внимание на @DevDatabase и @ProdDatabase на диаграмме. Позже вы увидите, что эти аннотации, по сути, являются стереотипами CDI,  и они будут очень полезны для решения нашего варианта использования.

Конфигурация источника данных

Консоль JBoss с источником данных

Консоль JBoss с тремя источниками данных

В Java EE немного болезненно то, как вы создаете и настраиваете новые источники данных. Благодаря   аннотации @DataSourceDefinition, добавленной в Java EE 6, теперь мы можем создавать новые источники данных,  используя аннотацию в нашем коде . Я не буду делать это здесь, так как я предпочитаю использовать CLI администратора JBoss. Следующие команды создадут источник данных для H2, Derby и Postgres (проверьте  сценарий jboss-setup.cli, потому что вам нужно  создать драйверы JDBC Derby и Postgres ), и результат можно увидеть в консоли JBoss:

/subsystem=datasources/data-source=H2DS:add(driver-name=h2, user-name=sa, password=sa, connection-url="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", jndi-name=java:/global/datasources/H2DS, enabled=true)
/subsystem=datasources/data-source=DerbyDS:add(driver-name=derby, user-name=app, password=app, connection-url="jdbc:derby://localhost:1527/sample;create=true", jndi-name=java:/global/datasources/DerbyDS, enabled=true)
/subsystem=datasources/data-source=PostgresDS:add(driver-name=postgres, user-name=postgres, password=postgres, connection-url="jdbc:postgresql://localhost:5432/postgres", jndi-name=java:/global/datasources/PostgresDS, enabled=true)

Теперь, когда наш JBoss знает эти три источника данных, давайте настроим их в нашем файле persistence.xml.

JPA Персистентные Единицы

В файле persistence.xml у нас может быть несколько единиц постоянства. Таким образом, идея состоит в том, чтобы создать три постоянных модуля, каждый из которых указывает на отдельный источник данных. Итак, мы будем иметь:

  • alternative-test-pu источник тестовых данных, указывающий на H2 в памяти в java: / global / datasources / H2DS
  • alternative-dev-pu источник данных разработки, указывающий на Derby на java: / global / datasources / DerbyDS
  • alternative-prod-pu источник производственных данных, указывающий на java: / global / datasources / PostgresDS

Вот как это выглядит в файле persistence.xml (для ясности я пропустил несколько свойств, вы можете  проверить здесь весь файл ):


persistence.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2.1"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="alternative-test-pu" transaction-type="JTA">
<jta-data-source>java:/global/datasources/H2DS</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="javax.persistence.sql-load-script-source" value="insert.sql"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.transaction.flush_before_completion" value="true"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
</properties>
</persistence-unit>
<persistence-unit name="alternative-dev-pu" transaction-type="JTA">
<jta-data-source>java:/global/datasources/DerbyDS</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="javax.persistence.sql-load-script-source" value="insert.sql"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.transaction.flush_before_completion" value="true"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect"/>
</properties>
</persistence-unit>
<persistence-unit name="alternative-prod-pu" transaction-type="JTA">
<jta-data-source>java:/global/datasources/PostgresDS</jta-data-source>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
<properties>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="javax.persistence.sql-load-script-source" value="insert.sql"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.transaction.flush_before_completion" value="true"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
</properties>
</persistence-unit>
</persistence>

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

Внедрение EntityManager

Как мы вводим EntityManager в Java EE 7? Итак, мы берем любой управляемый компонент, добавляем  аннотацию @Transactional, чтобы получить управляемую транзакционную демаркацию, и аннотация @PersistenceContext выполнит свою работу: с учетом единицы сохраняемости (здесь alternative-test-pu) контейнер будет внедрять EntityManager. В нашем приложении  SpeakerBean  является ответственным за выполнение CRUD-операций над   сущностью Speaker , поэтому мы внедряем EntityManager;


SpeakerBean
@Named
@Transactional
@ConversationScoped
public class SpeakerBean implements Serializable {
@PersistenceContext(unitName = "alternative-test-pu")
private EntityManager entityManager;
...

Но, как вы можете видеть из этого кода, здесь нет способа переключения между единицами постоянства, и, следовательно, нет способа переключения источников данных. Идея состоит в том, чтобы внедрить EntityManager с помощью аннотации CDI @Inject:


SpeakerBean
@Named
@Transactional
@ConversationScoped
public class SpeakerBean implements Serializable {
@Inject
private EntityManager entityManager;
...

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

Создание трех EntityManager

Как вы, возможно, уже знаете, единственный способ внедрить EntityManager в Java EE — это использовать  аннотацию @PersistenceContext или создать ее с помощью CDI (спасибо  производителям ). Возьмите любой POJO, добавьте любой атрибут или метод, аннотируйте его  @Produces , и результат будет внедрен CDI. Таким образом, приведенный ниже класс создаст наших трех EntityManager, каждый из которых указывает на свой постоянный модуль:


DatabaseProducer
public class DatabaseProducer {
@Produces
@PersistenceContext(unitName = "alternative-test-pu")
private EntityManager entityManagerTest;
@Produces
@PersistenceContext(unitName = "alternative-dev-pu")
private EntityManager entityManagerDev;
@Produces
@PersistenceContext(unitName = "alternative-prod-pu")
private EntityManager entityManagerProd;
}

Этот код не будет развернут, потому что развертывание неоднозначно. Если вы не  квалифицируете  продюсера, компонент или точку внедрения, он автоматически получает   квалификатор @Default . Итак, здесь три атрибута EntityManager имеют одинаковый спецификатор @Default, а CDI не знает, как сделать разницу между собой. Способ разделить их — создать разные классификаторы или стереотипы. Помните наш вариант использования: возможность переключения с одного источника данных на другой. Стереотипы — это то, что мы хотим. Так что нам действительно нужен следующий код (обратите внимание на   аннотацию @Alternative и оба стереотипа  @DevDatabase  и  @ProdDatabase ):


DatabaseProducer
public class DatabaseProducer {
@Produces
@PersistenceContext(unitName = "alternative-test-pu")
private EntityManager entityManagerTest;
@Produces
@Alternative
@DevDatabase
@PersistenceContext(unitName = "alternative-dev-pu")
private EntityManager entityManagerDev;
@Produces
@Alternative
@ProdDatabase
@PersistenceContext(unitName = "alternative-prod-pu")
private EntityManager entityManagerProd;
}

Способ чтения этого кода: « По умолчанию создайте тест EntityManager. Если альтернатива @DevDatabase активирована, создайте EntityManager для разработки. Если альтернатива @ProdDatabase активирована, создайте производственный EntityManager «.

Использование CDI стереотипов и альтернатив

В CDI  стереотип  инкапсулирует различные свойства, включая область видимости, привязки перехватчиков, квалификаторы, альтернативы и т. Д., В единый пакет многократного использования. С точки зрения кода, это просто аннотация Java с аннотацией  @Stereotype . Ниже представлен стереотип @DevDatabase:


DevDatabase
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface DevDatabase {
}

Код будет аналогичным для стереотипа @ProdDatabase. Теперь у нас есть один стереотип для базы данных разработки, один для производственной базы данных и ни одного для тестовой базы данных (так как он используется по умолчанию).

С другой стороны,  альтернативы  — это бины, реализация которых специфична для конкретного клиентского модуля или сценария развертывания. Это именно то, что мы хотим: в зависимости от нашего сценария развертывания (тестирование, разработка, производство) нам нужен другой EntityManager. Итак, давайте  объединим стереотипы и альтернативы,  чтобы получить то, что мы хотим.

Создание трех EntityManager со стереотипами и альтернативами

Стереотип может указывать, что компонент (или производитель), к которому он применяется, является  @Al альтернативой . Альтернативный стереотип позволяет нам классифицировать бины по сценарию развертывания. Итак, в наших стереотипах @DevDatabase и @ ProdDatabase нам нужно добавить   квалификатор @Alternative :


DevDatabase
@Stereotype
@Alternative
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface DevDatabase {
}

Благодаря этому стереотипу с альтернативой мы можем теперь переключать источник данных в   файлах beans.xml . Если файл пуст, альтернатива не активирована, поэтому будет применяться значение по умолчанию и будет использоваться тест EntityManager, указывающий на H2. Вместо этого, если мы хотим переключиться на производственную базу данных Postgres, нам нужно активировать альтернативу следующим образом:


beans.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bean-discovery-mode="all" version="1.1"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
<alternatives>
<stereotype>org.agoncal.sample.cdi.alternatives.database.cdi.ProdDatabase</stereotype>
</alternatives>
</beans>

Вывод

В этом посте я показал вам, как использовать CDI для переключения источников данных. CDI поставляется с мощной реализацией  шаблона проектирования моста  во имя  альтернатив . Это позволяет нам менять одну реализацию другой, в нашем случае, с одного EntityManager на другой. Будучи строго типизированным, единственный способ добиться этого в CDI — это использование  стереотипов  (CDI ненавидит внедрение с именами String) и создание соответствующего EntityManager, благодаря  Producers  andbeans.xml.

Кстати, спасибо JBoss Forge за то, что выкинули веб-приложение с помощью  нескольких команд .

Итак,  загрузите  код (или разветвите его на GitHub), попробуйте и дайте мне обратную связь.