«1. Обзор

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

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

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

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

2. Настройка Maven

Чтобы начать использовать 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.baeldung.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.baeldung.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 в указанном пакете:

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.
    }
}

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

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

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

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

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

классы DepartmentAbstract и EmployeeAbstract, содержащие методы для работы с сущностями. определены DepartmentListAbstract и EmployeeListAbstract, которые содержат методы для работы со списками отделов и сотрудников. CRUD-операции над нашими сущностями уже созданы для нас.

5. Приложение Reladomo

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

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

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

<MithraRuntime>
    <ConnectionManager 
      className="com.baeldung.reladomo.ReladomoConnectionManager ">
    <MithraObjectConfiguration 
      className="com.baeldung.reladomo.Department" cacheType="partial"/>
    <MithraObjectConfiguration 
      className="com.baeldung.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.baeldung.reladomo.Department" cacheType="partial"/>
        <MithraObjectConfiguration 
          className="com.baeldung.reladomo.Employee " cacheType="partial"/>
    </ConnectionManager>
 </MithraRuntime>

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

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

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

class com.baeldung.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. Заключение

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

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