DDD(Domain-driven design)の集約とJPA(Java Persistence API)のリレーションって相性悪い?

f:id:ts0818:20200815112337j:plain

nazology.net

「熱いもののほうが冷たいものより早く凍る現象(ムペンバ効果命名)」は、物理学における巨大な謎として現在まで君臨し続けていました。

お湯が冷水よりも早く凍る「ムペンバ効果」のナゾが解明される! | ナゾロジー

しかし今回、カナダのサイモンフレイザー大学の研究者たちにより、長年の謎解明につながる大きな発見がなされ、研究が世界で最も権威ある学術雑誌「Nature」に掲載されました。

お湯が冷水よりも早く凍る「ムペンバ効果」のナゾが解明される! | ナゾロジー

研究内容はカナダ、サイモンフレイザー大学のアビナッシュ・クマール氏らによってまとめられ、8月5日に世界で最も権威ある学術雑誌「Nature」に掲載されました。

Exponentially faster cooling in a colloidal system
https://www.nature.com/articles/s41586-020-2560-x

お湯が冷水よりも早く凍る「ムペンバ効果」のナゾが解明される! | ナゾロジー

⇧ 再現できないなら、解明されたことにはならない気はするけど...

この現象が実用的に適用されるところまで頑張って欲しいところですかね。

 

と言うわけで、今回は、Java関連の話で。レッツトライ~。

 

起きた事象

事の発端は、JavaのSpring BootでSpring DATA JPA を使って、Oracle Database にINSERTしようとして、

2020-08-14 17:26:59.751  WARN 2852 --- [nio-8080-exec-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 2291, SQLState: 23000
2020-08-14 17:26:59.751 ERROR 2852 --- [nio-8080-exec-4] o.h.engine.jdbc.spi.SqlExceptionHelper   : ORA-02291: 整合性制約(TS0818.SYS_C007973)に違反しました - 親キーがありません

2020-08-14 17:26:59.774 ERROR 2852 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [TS0818.SYS_C007973]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause

oracle.jdbc.OracleDatabaseException: ORA-02291: 整合性制約(TS0818.SYS_C007973)に違反しました - 親キーがありません
...省略

とかエラーになったんだけど、

teratail.com

org.postgresql.util.PSQLException: ERROR: 列licenses0_.user_uidは存在しません

外部キーが設定してありますから、親テーブル users の id が先に insert されていないと、子テーブル user_licenses の id は insert できません。
親テーブルを先にinsertしてから子テーブルをinsertすればcommitは後でも大丈夫です。

PostgreSQL - Spring Boot, JPAを使用したリレーション関係にあるテーブルへの一括INSERT|teratail

⇧ 上記サイト様で説明があるように、「親テーブルが先にINSERTされる必要があります」ということですと。

 

DDD(Domain-driven design)の集約と相反するJPAJava Persistence API

ここで、みんな大好きDDD(Domain-driven design)の集約とJPAJava Persistence API)のリレーションが相反してるんじゃないか問題が見えたという...

その前に、DDD(Domain-driven design)って何ぞ?

ドメイン駆動設計Domain-driven designDDD)とはソフトウェアの設計手法であり、「複雑なドメインの設計は、モデルベースで行うべき」であり、また「大半のソフトウェアプロジェクトでは、システムを実装するための特定の技術ではなく、ドメインそのものとドメインのロジックに焦点を置くべき」であるとする。この名称は、 Eric Evans が同名の著作で用いた

ドメイン駆動設計 - Wikipedia

⇧ 「ドメイン」に焦点を当ててシステムを構築していこうよってことですかね。

ドメイン」って言うのが何なのかって説明が無いんですな...

ドメインモデル」については、

ドメインモデルDomain model)は、システムに関わるさまざまな実体とそれらの関係を説明するシステムの概念モデルである。

ドメインモデル - Wikipedia

⇧ ってあるから、「ドメイン」ってのは、『システムに関わるさまざまな実体』ってことなんかね?

まぁ、何て言うか、一番重要な部分を曖昧にしてくれるのは本当にどうかと思うけど...

ドメイン」ってのは、「業務」に出てくる言葉の中で「システム」に影響しそうなものってことですかね?

で、「DDDの集約」と言うのは、

codezine.jp

⇧ 上記サイト様が分かりやすいですが、「注文処理」って業務があったとして、上記のように「ドメインモデル」として複数のオブジェクトで構成されるわけなんだけど、「注文」ってオブジェクトが「集約ルート」って呼ばれて、要するに上記の図で言うと「注文明細」「顧客ID」「商品ID」「配送先住所」などのオブジェクト群の「親」みたいなもんですかね。

  • 注文
    • 注文明細
    • 顧客ID
    • 商品ID
    • 配送先住所

 みたいな感じで、「注文」オブジェクトがその他のオブジェクトの取りまとめ役になるということですかね。

なので『集約』と呼ばれるということね。

 

一方で、JPAJava Persistence API)なんだが、

Java Persistence APIJPA)とは、関係データベースのデータを扱う Java SE および Java EE のアプリケーションを開発するためのJavaフレームワークである。

JPA は、以下の3つの部分から成る。

  • API(javax.persistence パッケージで定義されている)
  • Java Persistence Query Language
  • オブジェクト/関係メタデータ

JPAリファレンス実装EclipseLinkとして実装されている。

Java Persistence API - Wikipedia

⇧ ってな感じでリレーショナルデータベースをJavaで扱うためのフレームワークですと。

で、JPAに単純にDDDの「集約」パターンを当てはめようとすると、冒頭の問題が起きることになるんですと。

どういうことか?

例えば、

f:id:ts0818:20200815095912p:plain

っていうテーブルの関係があったとして、DDDの「集約」パターンでオブジェクトを構成しようとすると、

  • USERS
    • USER_DETAIL
    • AUTH_INFOMATION

ってな感じで、USERSってエンティティに「集約」されるはずだから、

package com.example.demo.domain.entity;

import java.sql.Timestamp;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "Users")
public class UsersEntity {

	@Id
	@Column(name="USER_ID")
	private String userId;
	@Column(name="INSERT_USER")
	private String insertUser;
	@Column(name="INSERT_DATE")
	private Timestamp insertDate;
	@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
	@JoinColumn(name = "USER_ID", referencedColumnName = "USER_ID", unique = true)
	private UserDetailEntity userDetailEntity;
	@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
	@JoinColumn(name="USER_ID", referencedColumnName = "USER_ID", unique = true)
	private AuthInfomationEntity authInfomationEntity;

}

みたいな感じにしたんですけど、

この構成で、普通にINSERTしようとすると、「USERS」テーブルにINSERTする前に、「USER_DETAIL」テーブルや「AUTH_INFOMATION」テーブルにINSERTしようとする挙動になるらしく、エラーが発生するんですと。

 

なので、解決方法としては、

  • 集約パターンを止める
  • SQLで頑張る

のどちらかになるとは思うんだけど、JPAってそもそもSQL書かなくて良いようにある程度デフォルトでメソッド用意されてる気がしたんで、SQLを自前でゴリゴリ実装するってのはJPAの思想に反する気もするんだが... 

とは言え、DDDの集約パターンを放棄するってのは、全体の設計の思想を考えるとどうなのかね...

と言う感じで、Javaってシンプルさを目指してはずなのに、シンプルでもなくなったし、複雑にした挙句に上手くいったかと言うと、あっちこっち破綻してる気がして仕方ない...

何だろう、結局のところ、フレームワークがイケてないのかね?

開発効率を上げるためのフレームワークのはずなのにね、妥協せざるを得ない仕組みになってるところからして、やる気を削がれるよね...

ちなみに、SQLで頑張る場合は、

stackoverflow.com

qiita.com

⇧ みたいな感じでイケそうですと。

 

そんな感じで、どうするべきか模索中ですかね...

って言うか、このあたりのベストプラクティスとかを有識者の方にまとめてもらいたいところですかね。

2020年8月23日(日)追記:↓ ここから

どうやら、「マルチテーブル・インサート」(複数テーブルに一度のクエリでデータをINSERTできる)ってのが、Oracle Database でしか利用できないみたい...

qiita.com

INSERT ALL 複数のテーブルに複数のレコードを一気に登録 Oracle - Qiita

INSERT ALL とは、Oracle で使えるマルチテーブルインサートと呼ばれるもの。
他の DB で同様の書き方という形で一緒に紹介されているのはマルチレコードインサート (そんな言葉はなさそう) と言ったほうが正しいもので、標準の書き方で複数のテーブルに一括でインサートできるのは Oracle のみの様子? (MySql は何やら複雑なことをしないとダメらしい)

INSERT ALL 複数のテーブルに複数のレコードを一気に登録 Oracle - Qiita

⇧ ってな感じで、「マルチレコードインサート」ってのは「マルチプルインサート」のことを言ってるんだとは思いますが、

www.atroom.info

バルクインサートとマルチプルインサートが今頃別物だと気付いたのでまとめる。ググってても勘違いしてるひと多い。

[MySQL]バルクインサートとマルチプルインサート|かわのくんのあたまんなか

⇧ 「バルクインサート」と「マルチプルインサート」が別物だったとは、確かに勘違いしておりました。

まぁ、というわけで、「SQLで頑張る」方法で複数テーブルに一度の処理でデータをINSERTするのは、データベースによっては実現できませんと、と言うか、2020年8月23日(日)時点だと、

の2つで実現できる可能性があるみたい、MySQLについては微妙なとこらしいですが...。 

実際の現場だとどうしてるのか知りたいところですわ。

気になったのは、

kagamihoge.hatenablog.com

「マルチテーブル・インサート文のどの部分にも順序を指定することはできません。」とか制限もあるし……

マルチテーブル・インサートで同一テーブルに複数データ挿入してみる - kagamihogeの日記

docs.oracle.com

マルチテーブル・インサート文のどの部分にも順序を指定することはできません。マルチテーブル・インサートは、単一のSQL文とみなされます。したがって、NEXTVALを初めて参照するときに、その次の番号が生成された後、この番号と同じ番号が、この文の後続のすべての参照で戻されます。

https://docs.oracle.com/cd/E57425_01/121/SQLRF/statements_9015.htm#i2125362

⇧ ってな感じで、「マルチテーブル・インサート文のどの部分にも順序を指定することはできません。」って言葉の意味がいまいち伝わってこないんだが、もし上記が、各INSERT文を実行する順番が指定できない、ってことを言ってるとすると、「親」「子」関係のあるテーブル(「外部キー制約」のあるテーブル)でマルチテーブル・インサート使えないやん...

ってなると、複数回クエリを実行するか、DDDの集約を止めるか、って選択になってくるんですかね?

まじか...

やっぱり、Java を取り巻く環境っていろいろ破綻してるように思えてしかたない...

2020年8月23日(日)追記:↑ ここまで

2020年8月27日(木)追記:↓ ここから

やはりと言うか、

dba.stackexchange.com

No, you can't depend on this. SQL is declarative, not procedural, so within a statement you can't guarantee the order of execution. Since the entire INSERT ALL statement is considered a single statement (doc), you can't guarantee that one INSERT will be before another.

oracle - Using Multi-table Insert for Parent and Child Table - Database Administrators Stack Exchange

⇧ マルチテーブル・インサートでのINSERTの順番が保障されないんだとか(涙)。

foreign key で親子関係になってしまってるテーブルでマルチテーブル・インサートはできないっちゅうことですかね...

 

2020年8月27日(木)追記:↑ ここまで

 

今回はこのへんで。