ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~4. Repository~

こんにちは、農業系プロダクトの開発を担当している糸井です。

弊社ではピンポイントタイム散布(以下、PTS)という防除のデジタル化サービスを展開しております。

www.optim.co.jp

PTSシステムのバックエンドではSpringBootを使用しております。
立ち上げ期より、既存の農業系プロダクトの課題感をもとに改善を進めてきましたので、その軌跡をご紹介します。

前回までの記事はこちら


前回の記事では、PublicSetterに対する改善事例をご紹介しました。
今回はRepositoryに対する改善事例をご紹介します。

4-1. Repository の引数と戻り値の型を統一する

背景と理由については LINEヤフー Tech Blog の以下の記事が分かりやすいと思いましたので、参照させていただきます。 (LINEヤフー株式会社様より転載の許諾を頂きました。ありがとうございます。

イベント駆動とドメインモデルの完全性を意識したアーキテクチャ設計
また、databaseに対する永続化を行うメソッドの引数に上記で定義した型定義を利用し、メソッドを一元化することでdatabaseに入りうるデータを常に上記の業務ロジックが満たされた状態を担保できます。
クーポンサービスの実装では、上記のメソッドを通じてのみdatabaseへデータを永続化できるようにしており、またこれ以外の永続化できるメソッドはありません。
Couponという型が引数になっているため、このインスタンスが生成できないと、そもそもdatabaseにデータを入れることができない仕組みになっています。
このようにして、ランタイムとしてアプリケーションが複数に分かれていても、データは常に同じ spec(仕様)を保つことができます。

前回の記事 3. PublicSetter にてDomainのEntityにロジックを詰め込めるようにしましたが、
それらのEntityを使用せずにRepositoryのメソッドを実装している箇所が多々あり、
同じspec(仕様)を保てていませんでした。

そこで、Repositoryの引数と戻り値の型をDomain Modelに従ったEntityで統一する運動を展開しました。

統一運動の結果

PublicSetterほど調査が簡単ではないので明確な数字で示すことはできませんが、
最近ではDomainのEntityを使用していないRepositoryのメソッドはあまり見かけなくなってきました。


4-2. RepositoryImpl の導入

実は、4-1. Repositoryの引数と戻り値の型を統一する をしようとすると、無理が出てきました。
PTSではORMapperにMybatisを使用していますが、Domain Modelに従ったEntityと、RDBのレコードが直接1対1でマッピングできなくなったからです。


例えば、1人のユーザーが複数のメールアドレスを持てるとします。 Domain ModelとしてはUserを集約Rootとして定義したので、Userクラスのメンバー変数にMailAddressを配列で持たせるように決めたとします。

package entity;

@AllArgsConstructor
public class User {
  private final String ulid;
  private final String name;
  private final List<MailAddress> mailAddressList;
}
package entity;

@AllArgsConstructor
public class MailAddress {
  private final String ulid;
  private final String userUlid;
  private final String address;
}

Userクラスを集約Rootとするので、Repositoryの引数と戻り値はUserクラスにする必要があります。


しかし、RDBは正規化されているので、usersテーブルと、mail_addressテーブルに分かれています。


なので、1回のCommand(insert, update, delete のSQL) では実装できません。


ということで、Domain Modelの集約RootとRDBの差分を吸収させる実装クラスとして、RepositoryImplを作ります。

UserRepositoryImpl は例えば下記のような実装になります。

package infrastructure;

@AllArgsConstructor
public class UserRepositoryImpl implements UserRepository {
  private final OrmUserRepositoryMapper ormUserRepositoryMapper;
  private final OrmMailAddressRepositoryMapper ormMailAddressRepositoryMapper;

  @Override
  public User select(String userUlid) {
    final OrmUser ormUser = ormUserRepositoryMapper.select(userUlid);
    final List<OrmMailAddress> ormMailAddressList = ormMailAddressRepositoryMapper.select(userUlid);

    return new User(
      ormUser.getUlid(),
      ormUser.getName(),
      ormMailAddressList
        .stream()
        .map(ormMailAddress -> new MailAddress(
          ormMailAddress.getUlid(),
          ormMailAddress.getUserUlid(),
          ormMailAddress.getMailAddress()
        ))
        .toList()
    );
  }
}

RepositoryImpl導入の結果

上記のように実装することで、Domain Modelの集約Rootと定義したEntityが複数テーブルに跨っている場合でも、
Repositoryの引数と戻り値は集約RootのEntityのままにし、同じ spec (仕様) を守れるようにしました。

つづきはこちら (2025/4/04 追記)

ピンポイントタイム散布(PTS)でのバックエンド改善の軌跡 ~5. CQRS ~ - OPTiM TECH BLOG

最後に

OPTiMではチームで協力し、難しく大きな課題を楽しみながらチャレンジしていきたいというメンバーを大募集しております。 ご興味がありましたら、下記フォームよりご応募ください。

www.optim.co.jp