【Java】Reladomoでデータ履歴管理の実装を簡略化する

# Reladomo とは？
![reladomo-200.png](https://qiita-image-store.s3.amazonaws.com/0/179364/339a4371-9dcb-3bc7-4cb3-ac4208a327a9.png)
Reladomo は、ゴールドマン・サックス社がOSSとして公開している Java ORM フレームワークです。
RDBMS での変更履歴管理が容易になる特徴的な機能があり、結構気に入ってます。

Reladomo では、エンティティを XML で定義することで、Java、 DDL を生成することができるんですが、
生成された API を活用することで、対応するテーブルへの CRUD 操作を行うことができます。

## この記事でやること

今回は、Reladomo の基本操作、データモデルの種類などを簡単に触れています。

その際、バイテンポラルデータモデル（後述）については、実物の挙動を見た方がわかりやすいと思うので、
Spring Boot と組み合わせた REST API のサンプルを用いて、見ていきたいと思います。
https://github.com/amtkxa/spring-boot-reladomo

## Reladomo によるデータ履歴管理について

データの変更履歴を追跡できるようにしておくためには、履歴データを保存する必要があり、
何らかの意味を持つ時間も一緒に保存する必要があります。

### Transaction Time, Vaild Time

Reladomo を活用すると、データの変更履歴を追跡するための2つの時間の管理を容易に行うことができます。

* Vaild Time （Business Time とも呼ばれる)
    * 現実世界で発生した時間。
* Transaction Time （Processing Time とも呼ばれる)
    * システムに変更が加えられた時間。

### データモデル

Transaction Time, Vaild Time を使うか使わないかで、以下の4つのデータモデルが考えられます。
Reladomo を使用すると、エンティティ別にデータモデルを選択するような柔軟な設計も可能です。

| データモデル | 説明 |
|-----------|-----|
| スナップショットデータモデル | どの時間ディメンションもサポートされない（履歴管理なし） |
| トランザクション時間データモデル | Transaction Time のみサポート |
| 有効時間データモデル | Vaild Time のみサポート |
| バイテンポラルデータモデル | Vaild Time と Transaction Time の両方をサポート |

# 実際に使ってみる（トランザクション時間データモデル）

まずは、Reladomo の基本的な使い方に触れながら、トランザクション時間データモデルを扱ってみようと思います。

## 簡単なドメインモデルの用意
Reladomo の機能を確認することが目的なので、それが実現できそうな簡単なドメインモデルを用意しました。
![50054750-b2e45800-0188-11e9-8aeb-fad5262e78d8.png](https://qiita-image-store.s3.amazonaws.com/0/179364/f23348bd-bf9a-8bba-fc71-dad8d4d78e17.png)

## Reladomo オブジェクトの XML ファイル作成
まず、データベースのテーブルに対応するオブジェクトを定義するXMLファイルを作成します。

```xml:Account.xml
<MithraObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              objectType="transactional"
              xsi:noNamespaceSchemaLocation="../config/mithraobject.xsd">
    <PackageName>com.amtkxa.springbootreladomosimple.domain.entity</PackageName>
    <ClassName>Account</ClassName>
    <DefaultTable>ACCOUNT</DefaultTable>

    <AsOfAttribute name="processingDate" fromColumnName="PROCESSING_DATE_FROM" toColumnName="PROCESSING_DATE_TO"
                   toIsInclusive="false"
                   isProcessingDate="true"
                   infinityDate="[com.gs.fw.common.mithra.util.DefaultInfinityTimestamp.getDefaultInfinity()]"
                   defaultIfNotSpecified="[com.gs.fw.common.mithra.util.DefaultInfinityTimestamp.getDefaultInfinity()]"
    />
    <Attribute name="accountId" javaType="int" columnName="ACCOUNT_ID" primaryKey="true"
               primaryKeyGeneratorStrategy="SimulatedSequence">
        <SimulatedSequence sequenceName="Account"
                           sequenceObjectFactoryName="com.amtkxa.springbootreladomosimple.infrastructure.util.ObjectSequenceObjectFactory"
                           hasSourceAttribute="false"
                           batchSize="1"
                           initialValue="1"
                           incrementSize="1"/>
    </Attribute>
    <Attribute name="customerId" javaType="int" columnName="CUSTOMER_ID" nullable="false"/>
    <Attribute name="balance" javaType="double" columnName="BALANCE"/>
</MithraObject>
```

各属性について簡単に説明すると、下記のような内容になっています。

| Element | 説明 |
|-----------|-----|
| PackageName | 生成されるオブジェクトが属するパッケージ。 |
| ClassName | オブジェクトのクラス名。 |
| DefaultTable | オブジェクトが対応するテーブル。 |
| Attribute | オブジェクトの属性。DefaultTable で定義したテーブルのカラムに対応している。 |
| AsOfAttribute | 履歴管理用のカラムに対応しており、from から to の2カラムで期間を表す。 |
| SimulatedSequence | INSERT 時に自動採番するシーケンス値。 |

また、ここで設定している内容に関する詳細については、`mithraobject.xsd` を参照すると確認できます。

## Java ファイル生成

先ほど定義した XML ファイルから、対応するオブジェクトの Java ファイルを生成することができます。

### MithraClassList の作成
```xml:MithraClassList.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Mithra xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="../config/mithraobject.xsd">
    <MithraObjectResource name="ObjectSequence"/>
    <MithraObjectResource name="Customer"/>
    <MithraObjectResource name="Account"/>
    <MithraObjectResource name="Transaction"/>
</Mithra>
```

### gen-reladomo タスクの定義
`pom.xml` に、`gen-reladomo` タスクを定義します。この Ant タスクは、`MithraClassList.xml` ファイルを基に、`MithraGenerator`を使用してJavaファイルを生成します。

```xml:pom.xml
<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/model/MithraClassList.xml"
            generateGscListMethod="true"
            generatedDir="${project.build.directory}/generated-sources/reladomo"
            nonGeneratedDir="${project.basedir}/src/main/java"
    />
</tasks>
```

ここでは、`generatedDir` と `nonGeneratedDir` で２箇所出力先を定義しています。

* `generatedDir`
    * `gen-reladomo` タスクが実行されるたびに、再作成する。
    * CRUD 操作などを実行するために必要なコードが含まれる抽象クラスとユーティリティクラスが生成される。
    * 変更やバージョン管理をするべきではないクラス。
* `nonGeneratedDir`
    * `gen-reladomo` タスクが実行されても、生成するクラスが既に存在していれば再作成しない。
    * `generatedDir` に生成されたクラスに依存する具象クラスが生成される。初期状態では、最低限必要なコンストラクタのみ記載されており、ビジネスロジックなどの実装はこちらに行う。
    * バージョン管理するべきクラス。

### gen-reladomo タスクの実行
`mvn clean install` などを実行することで、`gen-reladomo` タスクにより Java ファイルが生成されます。
`generatedDir` には、XML の変更に追従するために毎回再作成する抽象クラス、`nonGeneratedDir` には、ユーザが独自にビジネスロジックを追記する具象クラスが生成されます。生成される各クラスと依存関係は、以下のようになります。

![FireShot Capture 21 - spring-boot-relado_ - https___app.genmymodel.com_edit__alkjIAXvEemAl9N48If50Q#.png](https://qiita-image-store.s3.amazonaws.com/0/179364/bb7bb641-f8fa-48a6-3c6a-52d992f6c6d4.png)

ここで生成される抽象クラスには、エンティティに対してCRUD操作を実行するために必要なコードの大部分が含まれています。

* AccountAbstract
    * 単一レコードに対応したクラス。
* AccountListAbstract
    * 複数レコードに対応したクラス。
* AccountDatabaseObjectAbstract
    * コネクションやテーブルに関する情報が含まれるクラス。
* AccountFinder
    * 検索時に条件の指定などを行う処理が含まれるクラス。

## DDL生成
Reladomo で対応しているデータベースの DDL を生成することができます。DDL は、テーブル、インデックス、外部キーに対応しているようです。

### gen-reladomo-db タスクの定義
`pom.xml` に、`gen-reladomo-db` タスクを定義します。この Ant タスクは、`MithraClassList.xml` ファイルを基に、`MithraDbDefinitionGenerator` を使用して DDL を生成します。

```xml:pom.xml
<tasks>
    <property name="plugin_classpath" refid="maven.plugin.classpath"/>
    <taskdef name="gen-reladomo-db" classpath="plugin_classpath"
                classname="com.gs.fw.common.mithra.generator.dbgenerator.MithraDbDefinitionGenerator"/>
    <gen-reladomo-db
            xml="${project.basedir}/src/main/resources/reladomo/model/MithraClassList.xml"
            generatedDir="${project.build.directory}/generated-resources/db"
            databaseType="postgres"/>
</tasks>
```

### gen-reladomo-db タスクの実行
`mvn clean install` などを実行することで、`gen-reladomo-db` タスクにより DDL が生成されます。
`databaseType` に `postgres ` と設定した場合に生成された DDL は、下記の内容になっていました。

```sql:target/generated-resources/db/ACCOUNT.ddl
drop table if exists ACCOUNT;

create table ACCOUNT
(
    ACCOUNT_ID int not null,
    CUSTOMER_ID int not null,
    BALANCE float8,
    PROCESSING_DATE_FROM timestamp not null,
    PROCESSING_DATE_TO timestamp not null
);
```
```sql:target/generated-resources/db/ACCOUNT.idx
alter table ACCOUNT add constraint ACCOUNT_PK primary key (ACCOUNT_ID, PROCESSING_DATE_TO);

drop index if exists ACCOUNT_IDX0;
create index ACCOUNT_IDX0 on ACCOUNT(CUSTOMER_ID, PROCESSING_DATE_TO);
```

Reladomo を用いて履歴管理をする場合、該当の XML からの Foreign Key DDL は生成されないようです。
ちなみに、ここで生成される DDL はそのまま使用するものではなく、これを基にカスタマイズすることが想定されているようです。

> The DDLs generated by Reladomo are not meant to be used as is. They are merely meant to be a starting point for further customization. There are two common reasons to customize the generated DDLs. One is to add database specific clauses for specifying table spaces, schemas etc. Another is to add additional indices based on usage patterns seen in the application.
[Guided Tour Of Reladomo - 2.3. Database DDL Generation](https://goldmansachs.github.io/reladomo-kata/reladomo-tour-docs/tour-guide.html#N401CA)

DDL の生成機能はありがたいですね。

## Reladomo の CRUD 操作

### CRUD (Create)

#### 単一レコードの登録

Reladomo により生成された具象クラスに、次のようなコンストラクタを追加します。

```java:Account.java
public Account(int customerId, double balance) {
    this(com.gs.fw.common.mithra.util.DefaultInfinityTimestamp.getDefaultInfinity());
    this.setCustomerId(customerId);
    this.setBalance(balance);
}
```

その後、下記のコードを実行してみます。

```java
MithraManagerProvider.getMithraManager().executeTransactionalCommand((tx) -> {
    Account account = new Account(1, 100);
    account.insert();
    return null;
});
```

すると、接続先DBの ACCOUNT テーブルにレコードが登録されました。

```
testdb=> select * from account;
 account_id | customer_id | balance |  processing_date_from  | processing_date_to
------------+-------------+---------+------------------------+---------------------
          1 |           1 |     100 | 2018-12-23 14:42:14.61 | 9999-12-01 23:59:00
```

#### シーケンス値の採番

値を設定していない `account_id` には、採番されたシーケンス値がセットするようにしているので、そこの補足をします。
先ほど定義した Account.xml の該当列に、下記のような記載をするとシーケンス値の採番もできるようになります。

```xml:Account.xml
    <Attribute name="accountId" javaType="int" columnName="ACCOUNT_ID" primaryKey="true"
               primaryKeyGeneratorStrategy="SimulatedSequence">
        <SimulatedSequence sequenceName="Account"
                           sequenceObjectFactoryName="com.amtkxa.springbootreladomosimple.infrastructure.util.ObjectSequenceObjectFactory"
                           hasSourceAttribute="false"
                           batchSize="1"
                           initialValue="1"
                           incrementSize="1"/>
    </Attribute>
```

`Account.xml` で指定している `ObjectSequenceObjectFactory` は別途作成しています。

```java:ObjectSequenceObjectFactory.java
public class ObjectSequenceObjectFactory implements MithraSequenceObjectFactory {
  public MithraSequence getMithraSequenceObject(String sequenceName, Object sourceAttribute, int initialValue) {
    ObjectSequence objectSequence = ObjectSequenceFinder.findByPrimaryKey(sequenceName);
    if (objectSequence == null) {
      objectSequence = new ObjectSequence();
      objectSequence.setSimulatedSequenceName(sequenceName);
      objectSequence.setNextValue(initialValue);
      objectSequence.insert();
    }
    return objectSequence;
  }
}

```

採番する値は、OBJECT_SEQUENCE テーブルにて管理されていて、これも XML にてメタデータを定義しています。

```xml:ObjectSequence.xml
<MithraObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              objectType="transactional"
              xsi:noNamespaceSchemaLocation="../config/mithraobject.xsd">

    <PackageName>com.amtkxa.springbootreladomosimple.domain.entity</PackageName>
    <ClassName>ObjectSequence</ClassName>
    <DefaultTable>OBJECT_SEQUENCE</DefaultTable>

    <Attribute name="simulatedSequenceName" javaType="String" columnName="SEQUENCE_NAME" primaryKey="true" maxLength="64"/>
    <Attribute name="nextValue" javaType="long" columnName="NEXT_VALUE"/>

</MithraObject>
```

```
testdb=> select * from object_sequence;
 sequence_name | next_value
---------------+------------
 Account       |          2
```

#### 複数レコードの登録

Reladomo により生成された `AccountList` を使用することで、複数レコードの登録も可能です。

```java
MithraManagerProvider.getMithraManager().executeTransactionalCommand((tx) -> {
    AccountList accountList = new AccountList();
    accountList.add(new Account(2, 200));
    accountList.add(new Account(3, 300));
    accountList.insertAll();
    return null;
});
```

このコードを実行すると、複数レコード登録されました。

```
testdb=> select * from account where customer_id in (2, 3);
 account_id | customer_id | balance |  processing_date_from  | processing_date_to
------------+-------------+---------+------------------------+---------------------
          2 |           2 |     200 | 2018-12-23 15:10:37.92 | 9999-12-01 23:59:00
          3 |           3 |     300 | 2018-12-23 15:10:37.92 | 9999-12-01 23:59:00
```

### CRUD (Read)

検索処理は、Reladomo で生成された `Finder` クラスを利用します。

#### 検索条件の設定

`Finder` クラスを利用することで、`Operation` クラスのインスタンスを生成することができます。
`Operation` クラスは、発行されるクエリの WHERE 句となる部分を設定することができます。

いくつか例を挙げてみます。

```java
// where NAME = 'tanaka'
Operation op1 = CustomerFinder.name().eq("tanaka");
// where NAME <> 'tanaka'
Operation op2 = CustomerFinder.name().notEq("tanaka");
// where NAME like 'tanaka%'
Operation op3 = CustomerFinder.name().startsWith("tanaka");
// where NAME like '%taro'
Operation op4 = CustomerFinder.name().endsWith("taro");
// where NAME like '%tanaka%'
Operation op5 = CustomerFinder.name().wildCardEq("*tanaka*");
// where CUSTOMER_ID < 3
Operation op6 = CustomerFinder.customerId().lessThan(3);
```

ここであげている他にも、`isNull` や `contains` などが用意されています。
また、以下のように `Operation` を連鎖させることもできます。

```java
// where NAME like '%tanaka%' and CUSTOMER_ID < 3
Operation op = op5.and(op6);
```

#### 1件検索

`Finder` クラスの `findOne` を利用することで、単一レコードを取得することができます。

```java
// select t0.ACCOUNT_ID,t0.CUSTOMER_ID,t0.BALANCE,t0.PROCESSING_DATE_FROM,t0.PROCESSING_DATE_TO from ACCOUNT t0 
// where  t0.ACCOUNT_ID = 1 and t0.PROCESSING_DATE_TO = '9999-12-01 23:59:00.000'
Account account = AccountFinder.findOne(AccountFinder.accountId().eq(1));
```

実際に発行されたクエリの条件に、`t0.PROCESSING_DATE_TO = '9999-12-01 23:59:00.000'`とありますが、これは最新履歴に該当するレコードを指すものです。

#### 複数検索

`Finder` クラスの `findMany` を利用した場合は、複数レコードを List で取得することができます。

```java
// select t0.CUSTOMER_ID,t0.NAME,t0.COUNTRY,t0.PROCESSING_DATE_FROM,t0.PROCESSING_DATE_TO from CUSTOMER t0
// where  t0.NAME like 'tanaka%' and t0.PROCESSING_DATE_TO = '9999-12-01 23:59:00.000'
CustomerList customerList = CustomerFinder.findMany(CustomerFinder.name().startsWith("tanaka"));
```

### CRUD (Update)

`Finder` クラスを使って取得したオブジェクトに対して変更を加えると、変更内容が DB のテーブルにも反映されます。
挙動を確認するために、こんなデータを用意しました。

```
testdb=> select * from customer where customer_id = 7;
 customer_id |  name   | country |  processing_date_from  | processing_date_to
-------------+---------+---------+------------------------+---------------------
           7 | Pikachu | Japan   | 2018-12-23 19:36:07.06 | 9999-12-01 23:59:00
```

ピカチュウさんがライチュウさんに変わるので、変更を加えるために下記のコードが実行されたとします。

```java
MithraManagerProvider.getMithraManager().executeTransactionalCommand((tx) -> {
    Customer customer = CustomerFinder.findOne(CustomerFinder.customerId().eq(7));
    customer.setName("Raichu");
    return null;
});
```

実行結果は、以下のようになりました。

```
testdb=> select * from customer where customer_id = 7;
 customer_id |  name   | country |  processing_date_from  |   processing_date_to
-------------+---------+---------+------------------------+------------------------
           7 | Pikachu | Japan   | 2018-12-23 19:36:07.06 | 2018-12-23 19:46:13.91
           7 | Raichu  | Japan   | 2018-12-23 19:46:13.91 | 9999-12-01 23:59:00
```

実際に発行されたクエリは、下記の通りです。

```sql
-- 最新履歴のデータを取得
select t0.CUSTOMER_ID,t0.NAME,t0.COUNTRY,t0.PROCESSING_DATE_FROM,t0.PROCESSING_DATE_TO from CUSTOMER t0 
where  t0.CUSTOMER_ID = 7 and t0.PROCESSING_DATE_TO = '9999-12-01 23:59:00.000' FOR SHARE OF t0;

-- 最新履歴のデータの有効期間を更新
update CUSTOMER set PROCESSING_DATE_TO = '2018-12-23 19:46:13.910'  
where CUSTOMER_ID = 7 AND PROCESSING_DATE_TO = '9999-12-01 23:59:00.000';

-- 最新履歴として、変更後のデータを登録
insert into CUSTOMER(CUSTOMER_ID,NAME,COUNTRY,PROCESSING_DATE_FROM,PROCESSING_DATE_TO) 
values (7,'Raichu','Japan','2018-12-23 19:46:13.910','9999-12-01 23:59:00.000');
```

### CRUD (Delete)

`Finder` クラスを使って取得したオブジェクトを削除することで、テーブルにも反映することができます。
その場合、`delete()`, `terminate()`, `purge()` でそれぞれ挙動が異なるので、簡単に動かしてみます。

### delete()

`delete()` は、Reladomo で履歴管理をしないオブジェクト（XML に `AsOfAttribute` があるかにより判断）に対して使用できます。
これを実行すると、テーブルからレコードが物理削除されます。

```java
MithraManagerProvider.getMithraManager().executeTransactionalCommand((tx) -> {
    Transaction transaction = TransactionFinder.findOne(TransactionFinder.transactionId().eq(4));
    transaction.delete();
    return null;
});
```

実際に実行されたクエリは、以下の内容です。

```sql
select t0.LOG_ID,t0.ACCOUNT_ID,t0.TRANSACTION_TYPE,t0.AMOUNT,t0.TRANSACTION_DATE from TRANSACTION t0 
where  t0.LOG_ID = 4 FOR SHARE OF t0;

delete from TRANSACTION where LOG_ID = 4;
```

### terminate()

`terminate()` は、Reladomo で履歴管理をするオブジェクトの履歴を終了させます。（履歴としてのレコードは残る）
挙動を確認するために、以下のデータを用意しました。

```
testdb=> select * from customer where customer_id = 7;
 customer_id |  name   | country |  processing_date_from  |   processing_date_to
-------------+---------+---------+------------------------+------------------------
           7 | Pikachu | Japan   | 2018-12-23 19:36:07.06 | 2018-12-23 19:46:13.91
           7 | Raichu  | Japan   | 2018-12-23 19:46:13.91 | 9999-12-01 23:59:00
```

それに対して、このコードを実行します。

```java
MithraManagerProvider.getMithraManager().executeTransactionalCommand((tx) -> {
    Customer customer = CustomerFinder.findOne(CustomerFinder.customerId().eq(7));
    customer.terminate();
    return null;
});
```

すると、`processing_date_to` で `9999-12-01 23:59:00` と登録されていた箇所が更新されます。

```
testdb=> select * from customer where customer_id = 7;
 customer_id |  name   | country |  processing_date_from  |   processing_date_to
-------------+---------+---------+------------------------+------------------------
           7 | Pikachu | Japan   | 2018-12-23 19:36:07.06 | 2018-12-23 19:46:13.91
           7 | Raichu  | Japan   | 2018-12-23 19:46:13.91 | 2018-12-23 22:10:36.68
```

実際に実行されたクエリは、以下の内容です。

```sql
select t0.CUSTOMER_ID,t0.NAME,t0.COUNTRY,t0.PROCESSING_DATE_FROM,t0.PROCESSING_DATE_TO from CUSTOMER t0 
where  t0.CUSTOMER_ID = 7 and t0.PROCESSING_DATE_TO = '9999-12-01 23:59:00.000' FOR SHARE OF t0;

update CUSTOMER set PROCESSING_DATE_TO = '2018-12-23 22:10:36.680'  
where CUSTOMER_ID = 7 AND PROCESSING_DATE_TO = '9999-12-01 23:59:00.000';
```

### purge()

`purge()` は、Reladomo で履歴管理をするオブジェクトの履歴そのものを削除します。

```java
MithraManagerProvider.getMithraManager().executeTransactionalCommand((tx) -> {
    Customer customer = CustomerFinder.findOne(CustomerFinder.customerId().eq(6));
    customer.purge();
    return null;
});
```

実際に実行されたクエリは、以下の内容です。

```sql
select t0.CUSTOMER_ID,t0.NAME,t0.COUNTRY,t0.PROCESSING_DATE_FROM,t0.PROCESSING_DATE_TO from CUSTOMER t0
where  t0.CUSTOMER_ID = 6 and t0.PROCESSING_DATE_TO = '9999-12-01 23:59:00.000' FOR SHARE OF t0;

delete from CUSTOMER where CUSTOMER_ID = 6;
```

# 実際に使ってみる（バイテンポラルデータモデル）

Spring Boot と組み合わせてバイテンポラルデータモデルを扱う REST API のサンプルを使います。
https://github.com/amtkxa/spring-boot-reladomo

バイテンポラルデータモデルで扱う Transaction Time, Vaild Time は、同じタイミングになることも多いのですが、ここではその2つを同時に管理するメリットがわかるケースを辿ってみます。

* このケースは、[Guided Tour Of Reladomo](https://goldmansachs.github.io/reladomo-kata/reladomo-tour-docs/tour-guide.html) で扱っているものとほぼ同じ内容です。
* processing_date には、私が操作してシステムに反映された時間で履歴が登録されています。

Reladomo の基本的な使い方は先ほど触れているので、あまり被らない部分を紹介します。

## 1月1日 - 口座を初回入金額10000円で開設

一番最初の履歴レコードが登録されました。

```
sampledb=> select * from account where account_id = 10;
 account_id | customer_id | balance | business_date_from  |  business_date_to   |  processing_date_from  | processing_date_to
------------+-------------+---------+---------------------+---------------------+------------------------+---------------------
         12 |           1 |   10000 | 2019-01-01 00:00:00 | 9999-12-01 23:59:00 | 2018-12-22 16:17:17.49 | 9999-12-01 23:59:00
```

## 1月17日 - 5000円入金

何らかのトラブルにより、入金できていなかったとします。
そのため、テーブル内容に変更はありません。

## 1月20日 - 20000円入金

20000円入金されたことにより、履歴が切られました。

```
sampledb=> select * from account where account_id = 12 order by 4, 6;
 account_id | customer_id | balance | business_date_from  |  business_date_to   |  processing_date_from  |   processing_date_to
------------+-------------+---------+---------------------+---------------------+------------------------+------------------------
         12 |           1 |   10000 | 2019-01-01 00:00:00 | 9999-12-01 23:59:00 | 2018-12-22 16:33:09.54 | 2018-12-22 16:38:27.48
         12 |           1 |   10000 | 2019-01-01 00:00:00 | 2019-01-20 00:00:00 | 2018-12-22 16:38:27.48 | 9999-12-01 23:59:00
         12 |           1 |   30000 | 2019-01-20 00:00:00 | 9999-12-01 23:59:00 | 2018-12-22 16:38:27.48 | 9999-12-01 23:59:00
```

* Reladomo で生成された `Account` クラスの `incrementBalance()` を使った実装にしていると、入金額20000円が残高に加算される挙動になるよう、該当テーブルが更新されます。
* `processing_date_to` と `processing_date_to ` が `9999-12-01 23:59:00` のレコードが最新履歴です。

## 1月25日 - 入金できていなかった5000円の修正

1月17日 に入金していた分の金額の修正が、1月25日に発生したとします。
この場合、business_date に1月17日を指定して取得したオブジェクトに対し、`incrementBalance()` を使用して更新処理を行うと、該当テーブルが下記の内容に更新されます。

```
sampledb=> select * from account where account_id = 12 order by 6, 4;
 account_id | customer_id | balance | business_date_from  |  business_date_to   |  processing_date_from  |   processing_date_to
------------+-------------+---------+---------------------+---------------------+------------------------+------------------------
         12 |           1 |   10000 | 2019-01-01 00:00:00 | 9999-12-01 23:59:00 | 2018-12-22 16:33:09.54 | 2018-12-22 16:38:27.48
         12 |           1 |   10000 | 2019-01-01 00:00:00 | 2019-01-20 00:00:00 | 2018-12-22 16:38:27.48 | 2018-12-22 16:45:31.94
         12 |           1 |   30000 | 2019-01-20 00:00:00 | 9999-12-01 23:59:00 | 2018-12-22 16:38:27.48 | 2018-12-22 16:45:31.94
         12 |           1 |   10000 | 2019-01-01 00:00:00 | 2019-01-17 00:00:00 | 2018-12-22 16:45:31.94 | 9999-12-01 23:59:00
         12 |           1 |   15000 | 2019-01-17 00:00:00 | 2019-01-20 00:00:00 | 2018-12-22 16:45:31.94 | 9999-12-01 23:59:00
         12 |           1 |   35000 | 2019-01-20 00:00:00 | 9999-12-01 23:59:00 | 2018-12-22 16:45:31.94 | 9999-12-01 23:59:00
```

修正前の履歴データを保持したまま、修正が必要な履歴データの残高が更新できていることを確認できました。

# 最後に

Reladomo を実際に使ってみて、データ履歴管理が容易に実現できることを実感しました。
監査などのために変更履歴を後から追跡したいケースなどでは特に、開発効率の向上が期待できそうなので、
JVM 言語を利用する開発においては、役立つ場面が多いかもしれません。

また、エンティティごとのキャッシュ戦略選択や、Multi-Threaded matcher Loader (MTLoader) など、
Reladomo で提供されている高パフォーマンスを実現するための機能は特に気になっているので、
そちらも別の機会に調べたいと思います。


# 参考にさせて頂いた資料

今回調べてみて、英語のドキュメントが多いように感じましたが、質の高いものばかりでした。

* [Reladomo Documentation](https://goldmansachs.github.io/reladomo/)
* [Guided Tour Of Reladomo](https://goldmansachs.github.io/reladomo-kata/reladomo-tour-docs/tour-guide.html)
* [Introducing Reladomo - Enterprise Open Source Java ORM, Batteries Included!](https://www.infoq.com/articles/Reladomo-Open-Source-ORM)
* [Introducing Reladomo - Enterprise Open Source Java ORM, Batteries Included! (Part 2)](https://www.infoq.com/articles/Reladomo-Open-Source-ORM-Part2)

また、とてもわかりやすい日本語の資料もあり、本当に参考になりました。

* [データ履歴管理のためのテンポラルデータモデルとReladomoの紹介 #jjug_ccc #ccc_g3](https://www.slideshare.net/itohiro73/jjug-ccc-2017-spring-bitemporal-data-modeling-and-reladomo)
* [Reladomo入門 JJUGナイトセミナー #jjug](https://www.slideshare.net/itohiro73/reladomo-jjug-jjug)
* [だいたいよくわからないブログ - BiTemporal Data Modelに入門中](https://matsu-chara.hatenablog.com/entry/2017/04/01/110000)
* [だいたいよくわからないブログ - ReladomoというORMを触ってみた](https://matsu-chara.hatenablog.com/entry/2017/04/08/110000)