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-файле.
Давайте создадим простой пример с двумя сущностями: отделами и сотрудниками. Вот визуальное представление нашей модели предметной области:
Давайте определим первый файл 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 в указанном пакете:
Это простые классы, в которые мы можем добавить наш собственный код. Например, класс 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);
}
Эти классы основаны на абстрактных и служебных классах в папке сгенерированных источников/реладомо
:
Основные типы классов в этой папке:
классы 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 .