🍒

Doma のアグリゲート戦略(複数の関連エンティティを一度に取得)

2025/02/11に公開

この記事では Doma 3.4.0 で導入された アグリゲート戦略 の少し複雑な例を紹介します。

例題は TERASOLUNA Server Framework for Java (5.x) Development Guideline の
「データベースアクセス(MyBatis3編)」に記載されている「関連Entityを1回のSQLで取得する方法について」を参考にさせていただきます(ありがとうございます)。

上述の TERASOLUNA のガイドラインでは、MyBatis 3 を使って1度の SQL の実行で8つのテーブルを結合し、対応する7つのエンティティクラスへデータをマッピングしています。

本記事では、同等の処理を Doma ではどのように実行できるかを説明します。

なお、SQLのDDLおよび初期データのDMLは、TERASOLUNA のガイドラインに示されたものと同じものを使います。上記リンク先もあわせてご覧ください。

エンティティクラスの定義

7つのエンティティクラスです。

@Entity
@Table(name = "t_order")
public class Order {
  @Id public int id;
  @Association public OrderStatus orderStatus;
  @Association public List<OrderItem> orderItems = new ArrayList<>();
  @Association public List<OrderCoupon> orderCoupons = new ArrayList<>();
}

@Entity
@Table(name = "t_order_item")
public class OrderItem {
  @Id public int orderId;
  @Id public String itemCode;
  @Association public Item item;
  public int quantity;
}

@Entity
@Table(name = "t_order_coupon")
public class OrderCoupon {
  @Id public int orderId;
  @Id public String couponCode;
  @Association public Coupon coupon;
}

@Entity
@Table(name = "m_item")
public class Item {
  @Id public String code;
  public String name;
  public int price;
  @Association public List<Category> categories = new ArrayList<>();
}

@Entity
@Table(name = "m_category")
public class Category {
  @Id public String code;
  public String name;
}

@Entity
@Table(name = "m_coupon")
public class Coupon {
  @Id public String code;
  public String name;
  public int price;
}

@Entity
@Table(name = "c_order_status")
public class OrderStatus {
  @Id public String code;
  public String name;
}

説明を簡単にするため、getter/setter は設けずフィールドをすべて public にしています。

アグリゲート戦略

アグリゲート戦略を以下のように記述します。

@AggregateStrategy(root = Order.class, tableAlias = "o")
interface OrderStrategy {

  @AssociationLinker(propertyPath = "orderStatus", tableAlias = "os")
  BiFunction<Order, OrderStatus, Order> orderStatus =
      (o, os) -> {
        o.orderStatus = os;
        return o;
      };

  @AssociationLinker(propertyPath = "orderItems", tableAlias = "oi")
  BiFunction<Order, OrderItem, Order> orderItems =
      (o, oi) -> {
        o.orderItems.add(oi);
        return o;
      };

  @AssociationLinker(propertyPath = "orderCoupons", tableAlias = "oc")
  BiFunction<Order, OrderCoupon, Order> orderCoupons =
      (o, oc) -> {
        o.orderCoupons.add(oc);
        return o;
      };

  @AssociationLinker(propertyPath = "orderItems.item", tableAlias = "i")
  BiFunction<OrderItem, Item, OrderItem> orderItems$item =
      (oi, i) -> {
        oi.item = i;
        return oi;
      };

  @AssociationLinker(propertyPath = "orderCoupons.coupon", tableAlias = "cp")
  BiFunction<OrderCoupon, Coupon, OrderCoupon> orderCoupon$coupon =
      (oc, cp) -> {
        oc.coupon = cp;
        return oc;
      };

  @AssociationLinker(propertyPath = "orderItems.item.categories", tableAlias = "ct")
  BiFunction<Item, Category, Item> orderItems$item$categories =
      (i, c) -> {
        i.categories.add(c);
        return i;
      };
}

アグリゲート戦略では、エンティティ間の関連付けや SQL(のテーブルエイリアス) との紐付けを行います。全く同じものではないですが、強いて言えば MyBatis の resultMapcollectionassociation の定義に相当します。

propertyPath にはルートのエンティティからターゲットのプロパティまでのプロパティ名をドット(.)で繋げた文字列を指定します。

@AssociationLinker を注釈するフィールドの名前に制約はありませんが、重複を避けるため、 propertyPath.$ に置き換えたものにしています。

DAO

DAOを次のように定義します。

@Dao
public interface OrderDao {

  @Sql(
      """
  SELECT
      /*%expand */*
  FROM
      t_order o
      INNER JOIN c_order_status os ON os.code = o.status_code
      INNER JOIN t_order_item oi ON oi.order_id = o.id
      INNER JOIN m_item i ON i.code = oi.item_code
      INNER JOIN m_item_category ic ON ic.item_code = i.code
      INNER JOIN m_category ct ON ct.code = ic.category_code
      LEFT JOIN t_order_coupon oc ON oc.order_id = o.id
      LEFT JOIN m_coupon cp ON cp.code = oc.coupon_code
  WHERE
      o.id = /* id */0
  ORDER BY
      i_code ASC,
      ct_code ASC,
      cp_code ASC
  """)
  @Select(aggregateStrategy = OrderStrategy.class)
  Order findById(int id);
}

@SelectaggregateStrategy には、アグリゲート戦略の節で作成した OrderStrategy を指定します。

@SQL には、 8つのテーブルを結合したSQLを指定します。 各テーブルのエイリアスには OrderStrategy の中で指定したものを使います。

以上で定義は終わりです。

DAO の findById を実行すると、関連エンティティが紐づけられた Order エンティティを取得できます。

(参考)`findById` を呼び出すコード
  @Test
  public void test_orderId_1(Config config) {
    OrderDao dao = new OrderDaoImpl(config);
    Order order = dao.findById(1);

    {
      OrderStatus orderStatus = order.orderStatus;
      assertNotNull(orderStatus);
      assertEquals("accepted", orderStatus.code);
    }

    {
      List<OrderItem> orderItems = order.orderItems;
      assertEquals(2, orderItems.size());
      OrderItem orderItem1 = orderItems.get(0);
      OrderItem orderItem2 = orderItems.get(1);
      assertEquals("ITM0000001", orderItem1.itemCode);
      assertEquals("ITM0000002", orderItem2.itemCode);
      Item item1 = orderItem1.item;
      Item item2 = orderItem2.item;
      assertEquals("Orange juice", item1.name);
      assertEquals("NotePC", item2.name);

      assertEquals(1, item1.categories.size());
      Category item1Category1 = item1.categories.get(0);
      assertEquals("CTG0000001", item1Category1.code);
      assertEquals(2, item2.categories.size());
      Category item2Category1 = item2.categories.get(0);
      assertEquals("CTG0000002", item2Category1.code);
      Category item2Category2 = item2.categories.get(1);
      assertEquals("CTG0000003", item2Category2.code);
    }

    {
      List<OrderCoupon> orderCoupons = order.orderCoupons;
      assertEquals(2, orderCoupons.size());
      OrderCoupon orderCoupon1 = orderCoupons.get(0);
      assertEquals("CPN0000001", orderCoupon1.couponCode);
      OrderCoupon orderCoupon2 = orderCoupons.get(1);
      assertEquals("CPN0000002", orderCoupon2.couponCode);
      Coupon orderCoupon1Coupon = orderCoupon1.coupon;
      assertEquals(3000, orderCoupon1Coupon.price);
      Coupon orderCoupon2Coupon = orderCoupon2.coupon;
      assertEquals(30000, orderCoupon2Coupon.price);
    }
  }

まとめ

この記事では、Doma のアグリゲート戦略を使うことで複数の関連エンティティを一度に取得できることを示しました。

アグリゲート戦略の記述に慣れは必要かもしれませんが、個人的には MyBatis と比較してもシンプルな設定で同等のことが実現できているのではと思います。

ぜひ Doma のアグリゲート戦略をお試しください。そしてフィードバックをよろしくお願いします。

Discussion