Перейти к основному содержимому

Введение в Реладомо

· 10 мин. чтения

1. Обзор

Reladomo (ранее известный как Mithra) — это фреймворк объектно-реляционного отображения (ORM) для Java , разработанный в Goldman Sachs , в настоящее время выпущенный как проект с открытым исходным кодом. Фреймворк предоставляет функции, обычно необходимые для ORM, а также некоторые дополнительные.

Давайте посмотрим на некоторые из ключевых особенностей Reladomo :

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

В следующих разделах мы увидим настройку и несколько основных примеров использования.

2. Настройка Мавена

Чтобы начать использовать ORM, нам нужно добавить зависимость reladomo в наш файл pom.xml :

<dependency>
<groupId>com.goldmansachs.reladomo</groupId>
<artifactId>reladomo</artifactId>
<version>16.5.1</version>
</dependency>

Мы будем использовать базу данных H2 для наших примеров, поэтому давайте также добавим зависимость h2 :

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.196</version>
</dependency>

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

Для генерации файлов мы можем использовать задачи, которые выполняются с помощью maven-antrun-plugin . Во-первых, давайте посмотрим, как мы можем определить задачу для создания классов Java:

<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>generateMithra</id>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<property name="plugin_classpath"
refid="maven.plugin.classpath"/>
<taskdef name="gen-reladomo"
classpath="plugin_classpath"
classname="com.gs.fw.common.mithra.generator.MithraGenerator"/>
<gen-reladomo
xml="${project.basedir}/src/main/resources/reladomo/ReladomoClassList.xml"
generateGscListMethod="true"
generatedDir="${project.build.directory}/generated-sources/reladomo"
nonGeneratedDir="${project.basedir}/src/main/java"/>
</tasks>
</configuration>
</execution>
</executions>
</plugin>

Задача gen-reladomo использует предоставленный MithraGenerator для создания файлов Java на основе конфигурации в файле ReladomoClassList.xml . Мы более подробно рассмотрим, что содержит этот файл, в следующем разделе.

У задач также есть два свойства, которые определяют расположение сгенерированных файлов:

  • generateDir — содержит классы, которые не следует модифицировать или версионировать.
  • nonGeneratedDir — сгенерированные конкретные классы объектов, которые можно дополнительно настраивать и версионировать.

Таблицы базы данных, соответствующие объектам Java, могут быть созданы вручную или автоматически с помощью сценариев DDL, созданных второй задачей Ant :

<taskdef 
name="gen-ddl"
classname = "com.gs.fw.common.mithra.generator.dbgenerator.MithraDbDefinitionGenerator"
loaderRef="reladomoGenerator">
<classpath refid="maven.plugin.classpath"/>
</taskdef>
<gen-ddl
xml="${project.basedir}/src/main/resources/reladomo/ReladomoClassList.xml"
generatedDir="${project.build.directory}/generated-db/sql"
databaseType="postgres"/>

В этой задаче используется MithraDbDefinitionGenerator на основе того же файла ReladomoClassList.xml , о котором упоминалось ранее. Сценарии SQL будут размещены в каталоге generate-db/sql .

Чтобы завершить определение этого плагина, мы также должны добавить две зависимости, используемые для создания:

<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
//...
</executions>
<dependencies>
<dependency>
<groupId>com.goldmansachs.reladomo</groupId>
<artifactId>reladomogen</artifactId>
<version>16.5.1</version>
</dependency>
<dependency>
<groupId>com.goldmansachs.reladomo</groupId>
<artifactId>reladomo-gen-util</artifactId>
<version>16.5.1</version>
</dependency>
</dependencies>
</plugin>

Наконец, используя build-helper-maven-plugin , мы можем добавить сгенерированные файлы в путь к классам:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/reladomo</source>
</sources>
</configuration>
</execution>
<execution>
<id>add-resource</id>
<phase>generate-resources</phase>
<goals>
<goal>add-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>${project.build.directory}/generated-db/</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>

Добавление сценариев DDL не является обязательным. В нашем примере мы будем использовать базу данных в памяти, поэтому мы хотим выполнить сценарии для создания таблиц.

3. XML-конфигурация

Метаданные для платформы Reladomo могут быть определены в нескольких XML-файлах.

3.1. XML-файлы объектов

Каждая сущность, которую мы хотим создать, должна быть определена в ее XML-файле.

Давайте создадим простой пример с двумя сущностями: отделами и сотрудниками. Вот визуальное представление нашей модели предметной области:

./a81000c8c4df2ff1c096cd8c6a147d24.png

Давайте определим первый файл Department.xml :

<MithraObject objectType="transactional">
<PackageName>com.foreach.reladomo</PackageName>
<ClassName>Department</ClassName>
<DefaultTable>departments</DefaultTable>

<Attribute name="id" javaType="long"
columnName="department_id" primaryKey="true"/>
<Attribute name="name" javaType="String"
columnName="name" maxLength="50" truncate="true"/>
<Relationship name="employees" relatedObject="Employee"
cardinality="one-to-many"
reverseRelationshipName="department"
relatedIsDependent="true">
Employee.departmentId = this.id
</Relationship>
</MithraObject>

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

Каждое свойство типа определяется с помощью элемента Attribute , для которого мы можем указать имя, тип Java и имя столбца.

Мы можем описать отношения между объектами, используя тег Relationship . В нашем примере мы определили отношение « один ко многим » между объектами « Отдел » и « Сотрудник » на основе выражения:

Employee.departmentId = this.id

Атрибут reverseRelationshipName можно использовать для того, чтобы сделать отношение двунаправленным, не определяя его дважды.

Атрибут relatedIsDependent позволяет нам каскадировать операции.

Далее создадим файл Employee.xml аналогичным образом:

<MithraObject objectType="transactional">
<PackageName>com.foreach.reladomo</PackageName>
<ClassName>Employee</ClassName>
<DefaultTable>employees</DefaultTable>

<Attribute name="id" javaType="long"
columnName="employee_id" primaryKey="true"/>
<Attribute name="name" javaType="String"
columnName="name" maxLength="50" truncate="true"/>
<Attribute name="departmentId" javaType="long"
columnName="department_id"/>
</MithraObject>

3.2. Файл ReladomoClassList.xml

Reladomo нужно сообщить об объектах, которые он должен генерировать.

В разделе Maven мы определили файл ReladomoClassList.xml как источник для задач генерации, поэтому пришло время создать файл:

<Mithra>
<MithraObjectResource name="Department"/>
<MithraObjectResource name="Employee"/>
</Mithra>

Это простой файл, содержащий список сущностей, для которых будут созданы классы на основе конфигурации XML.

4. Сгенерированные классы

Теперь у нас есть все элементы, необходимые для начала генерации кода путем сборки приложения Maven с помощью команды mvn clean install .

Конкретные классы будут сгенерированы в папке src/main/java в указанном пакете:

./d531224ecfe83cc9eabb47150e35d926.png

Это простые классы, в которые мы можем добавить наш собственный код. Например, класс Department содержит только конструктор, который нельзя удалять:

public class Department extends DepartmentAbstract {
public Department() {
super();
// You must not modify this constructor. Mithra calls this internally.
// You can call this constructor. You can also add new constructors.
}
}

Если мы хотим добавить в этот класс пользовательский конструктор, он также должен вызвать родительский конструктор:

public Department(long id, String name){
super();
this.setId(id);
this.setName(name);
}

Эти классы основаны на абстрактных и служебных классах в папке сгенерированных источников/реладомо :

./daf712fa151645ef3a08b61a5c7c36ed.png

Основные типы классов в этой папке:

  • классы DepartmentAbstract и EmployeeAbstract , которые содержат методы для работы с определенными сущностями.
  • DepartmentListAbstract и EmployeeListAbstract — содержит методы для работы со списками отделов и сотрудников.
  • DepartmentFinder и EmployeeFinder — они предоставляют методы для запроса сущностей.
  • другие полезные классы

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

5. Приложение Реладомо

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

5.1. Диспетчер соединений

При работе с одной базой данных мы можем реализовать интерфейс SourcelessConnectionManager :

public class ReladomoConnectionManager implements SourcelessConnectionManager {

private static ReladomoConnectionManager instance;
private XAConnectionManager xaConnectionManager;

public static synchronized ReladomoConnectionManager getInstance() {
if (instance == null) {
instance = new ReladomoConnectionManager();
}
return instance;
}

private ReladomoConnectionManager() {
this.createConnectionManager();
}
//...
}

Наш класс ReladomoConnectionManager реализует шаблон singleton и основан на XAConnectionManager , который является служебным классом для диспетчера транзакционных соединений.

Рассмотрим подробнее метод createConnectionManager() :

private XAConnectionManager createConnectionManager() {
xaConnectionManager = new XAConnectionManager();
xaConnectionManager.setDriverClassName("org.h2.Driver");
xaConnectionManager.setJdbcConnectionString("jdbc:h2:mem:myDb");
xaConnectionManager.setJdbcUser("sa");
xaConnectionManager.setJdbcPassword("");
xaConnectionManager.setPoolName("My Connection Pool");
xaConnectionManager.setInitialSize(1);
xaConnectionManager.setPoolSize(10);
xaConnectionManager.initialisePool();
return xaConnectionManager;
}

В этом методе мы установили свойства, необходимые для создания подключения к базе данных H2 в памяти.

Также нам нужно реализовать несколько методов из интерфейса SourcelessConnectionManager :

@Override
public Connection getConnection() {
return xaConnectionManager.getConnection();
}

@Override
public DatabaseType getDatabaseType() {
return H2DatabaseType.getInstance();
}

@Override
public TimeZone getDatabaseTimeZone() {
return TimeZone.getDefault();
}

@Override
public String getDatabaseIdentifier() {
return "myDb";
}

@Override
public BulkLoader createBulkLoader() throws BulkLoaderException {
return null;
}

Наконец, давайте добавим пользовательский метод для выполнения сгенерированных сценариев DDL, которые создают наши таблицы базы данных:

public void createTables() throws Exception {
Path ddlPath = Paths.get(ClassLoader.getSystemResource("sql").toURI());
try (
Connection conn = xaConnectionManager.getConnection();
Stream<Path> list = Files.list(ddlPath)) {

list.forEach(path -> {
try {
RunScript.execute(conn, Files.newBufferedReader(path));
}
catch (SQLException | IOException exc){
exc.printStackTrace();
}
});
}
}

Это, конечно, не обязательно для производственного приложения, где ваши таблицы не будут пересоздаваться для каждого выполнения.

5.2. Инициализация Реладомо

В процессе инициализации Reladomo используется файл конфигурации, в котором указывается класс диспетчера соединений и используемые типы объектов. Давайте определим файл ReladomoRuntimeConfig.xml :

<MithraRuntime>
<ConnectionManager
className="com.foreach.reladomo.ReladomoConnectionManager ">
<MithraObjectConfiguration
className="com.foreach.reladomo.Department" cacheType="partial"/>
<MithraObjectConfiguration
className="com.foreach.reladomo.Employee " cacheType="partial"/>
</ConnectionManager>
</MithraRuntime>

Затем мы можем создать основной класс, в котором мы сначала вызываем метод createTables() , а затем используем класс MithraManager для загрузки конфигурации и инициализации Reladomo :

public class ReladomoApplication {
public static void main(String[] args) {
try {
ReladomoConnectionManager.getInstance().createTables();
} catch (Exception e1) {
e1.printStackTrace();
}
MithraManager mithraManager = MithraManagerProvider.getMithraManager();
mithraManager.setTransactionTimeout(120);

try (InputStream is = ReladomoApplication.class.getClassLoader()
.getResourceAsStream("ReladomoRuntimeConfig.xml")) {
MithraManagerProvider.getMithraManager()
.readConfiguration(is);

//execute operations
}
catch (IOException exc){
exc.printStackTrace();
}
}
}

5.3. Выполнение CRUD-операций

Теперь давайте воспользуемся сгенерированными Reladomo классами для выполнения нескольких операций над нашими сущностями.

Сначала создадим два объекта Department и Employee , а затем сохраним оба с помощью метода cascadeInsert() :

Department department = new Department(1, "IT");
Employee employee = new Employee(1, "John");
department.getEmployees().add(employee);
department.cascadeInsert();

Каждый объект также можно сохранить отдельно, вызвав метод insert() . В нашем примере можно использовать cascadeInsert() , потому что мы добавили атрибут relatedIsDependent=true в определение нашего отношения.

Для запроса объектов мы можем использовать сгенерированные классы Finder :

Department depFound = DepartmentFinder
.findByPrimaryKey(1);
Employee empFound = EmployeeFinder
.findOne(EmployeeFinder.name().eq("John"));

Объекты, полученные таким образом, являются «живыми» объектами, то есть любое изменение их с помощью сеттеров немедленно отражается в базе данных:

empFound.setName("Steven");

Чтобы избежать такого поведения, мы можем получить отдельные объекты:

Department depDetached = DepartmentFinder
.findByPrimaryKey(1).getDetachedCopy();

Чтобы удалить объекты, мы можем использовать метод delete() :

empFound.delete();

5.4. Управление транзакциями

Если мы хотим, чтобы набор операций выполнялся или не выполнялся как одно целое, мы можем обернуть их в транзакцию:

mithraManager.executeTransactionalCommand(tx -> {
Department dep = new Department(2, "HR");
Employee emp = new Employee(2, "Jim");
dep.getEmployees().add(emp);
dep.cascadeInsert();
return null;
});

6. Тестовая поддержка Reladomo

В разделах выше мы написали наши примеры в основном классе Java.

Если мы хотим написать тесты для нашего приложения, один из способов сделать это — просто написать тот же код в тестовом классе.

Однако для лучшей поддержки тестирования Reladomo также предоставляет класс MithraTestResource . Это позволяет нам использовать другую конфигурацию и базу данных в памяти только для тестов.

Во- первых, нам нужно добавить дополнительную зависимость reladomo-test-util вместе с зависимостью junit :

<dependency>
<groupId>com.goldmansachs.reladomo</groupId>
<artifactId>reladomo-test-util</artifactId>
<version>16.5.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

Далее нам нужно создать файл ReladomoTestConfig.xml , в котором используется класс ConnectionManagerForTests :

<MithraRuntime>
<ConnectionManager
className="com.gs.fw.common.mithra.test.ConnectionManagerForTests">
<Property name="resourceName" value="testDb"/>
<MithraObjectConfiguration
className="com.foreach.reladomo.Department" cacheType="partial"/>
<MithraObjectConfiguration
className="com.foreach.reladomo.Employee " cacheType="partial"/>
</ConnectionManager>
</MithraRuntime>

Этот диспетчер соединений настраивает базу данных H2 в памяти, используемую только для тестов.

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

class com.foreach.reladomo.Department
id, name
1, "Marketing"

class com.foreach.reladomo.Employee
id, name
1, "Paul"

Давайте создадим тестовый класс JUnit и настроим наш экземпляр MithraTestResource в методе @Before :

public class ReladomoTest {
private MithraTestResource mithraTestResource;

@Before
public void setUp() throws Exception {
this.mithraTestResource
= new MithraTestResource("reladomo/ReladomoTestConfig.xml");

ConnectionManagerForTests connectionManager
= ConnectionManagerForTests.getInstanceForDbName("testDb");
this.mithraTestResource.createSingleDatabase(connectionManager);
mithraTestResource.addTestDataToDatabase("reladomo/test-data.txt",
connectionManager);

this.mithraTestResource.setUp();
}
}

Затем мы можем написать простой метод @Test , который проверяет, были ли загружены наши тестовые данные:

@Test
public void whenGetTestData_thenOk() {
Employee employee = EmployeeFinder.findByPrimaryKey(1);
assertEquals(employee.getName(), "Paul");
}

После выполнения тестов необходимо очистить тестовую базу данных:

@After
public void tearDown() throws Exception {
this.mithraTestResource.tearDown();
}

7. Заключение

В этой статье мы рассмотрели основные возможности фреймворка Reladomo ORM, а также настройку и примеры общего использования.

Исходный код примеров можно найти на GitHub .