しかし今回、カナダのサイモンフレイザー大学の研究者たちにより、長年の謎解明につながる大きな発見がなされ、研究が世界で最も権威ある学術雑誌「Nature」に掲載されました。
研究内容はカナダ、サイモンフレイザー大学のアビナッシュ・クマール氏らによってまとめられ、8月5日に世界で最も権威ある学術雑誌「Nature」に掲載されました。
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)に違反しました - 親キーがありません ...省略
とかエラーになったんだけど、
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)の集約と相反するJPA(Java Persistence API)
ここで、みんな大好きDDD(Domain-driven design)の集約とJPA(Java Persistence API)のリレーションが相反してるんじゃないか問題が見えたという...
その前に、DDD(Domain-driven design)って何ぞ?
ドメイン駆動設計(英: Domain-driven design, DDD)とはソフトウェアの設計手法であり、「複雑なドメインの設計は、モデルベースで行うべき」であり、また「大半のソフトウェアプロジェクトでは、システムを実装するための特定の技術ではなく、ドメインそのものとドメインのロジックに焦点を置くべき」であるとする。この名称は、 Eric Evans が同名の著作で用いた。
⇧ 「ドメイン」に焦点を当ててシステムを構築していこうよってことですかね。
「ドメイン」って言うのが何なのかって説明が無いんですな...
「ドメインモデル」については、
⇧ ってあるから、「ドメイン」ってのは、『システムに関わるさまざまな実体』ってことなんかね?
まぁ、何て言うか、一番重要な部分を曖昧にしてくれるのは本当にどうかと思うけど...
「ドメイン」ってのは、「業務」に出てくる言葉の中で「システム」に影響しそうなものってことですかね?
で、「DDDの集約」と言うのは、
⇧ 上記サイト様が分かりやすいですが、「注文処理」って業務があったとして、上記のように「ドメインモデル」として複数のオブジェクトで構成されるわけなんだけど、「注文」ってオブジェクトが「集約ルート」って呼ばれて、要するに上記の図で言うと「注文明細」「顧客ID」「商品ID」「配送先住所」などのオブジェクト群の「親」みたいなもんですかね。
- 注文
- 注文明細
- 顧客ID
- 商品ID
- 配送先住所
みたいな感じで、「注文」オブジェクトがその他のオブジェクトの取りまとめ役になるということですかね。
なので『集約』と呼ばれるということね。
一方で、JPA(Java Persistence API)なんだが、
Java Persistence API(JPA)とは、関係データベースのデータを扱う Java SE および Java EE のアプリケーションを開発するためのJava用フレームワークである。
JPA は、以下の3つの部分から成る。
JPAのリファレンス実装はEclipseLinkとして実装されている。
⇧ ってな感じでリレーショナルデータベースをJavaで扱うためのフレームワークですと。
で、JPAに単純にDDDの「集約」パターンを当てはめようとすると、冒頭の問題が起きることになるんですと。
どういうことか?
例えば、
っていうテーブルの関係があったとして、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で頑張る場合は、
⇧ みたいな感じでイケそうですと。
そんな感じで、どうするべきか模索中ですかね...
って言うか、このあたりのベストプラクティスとかを有識者の方にまとめてもらいたいところですかね。
2020年8月23日(日)追記:↓ ここから
どうやら、「マルチテーブル・インサート」(複数テーブルに一度のクエリでデータをINSERTできる)ってのが、Oracle Database でしか利用できないみたい...
INSERT ALL 複数のテーブルに複数のレコードを一気に登録 Oracle - Qiita
INSERT ALL とは、Oracle で使えるマルチテーブルインサートと呼ばれるもの。
他の DB で同様の書き方という形で一緒に紹介されているのはマルチレコードインサート (そんな言葉はなさそう) と言ったほうが正しいもので、標準の書き方で複数のテーブルに一括でインサートできるのは Oracle のみの様子? (MySql は何やら複雑なことをしないとダメらしい)
⇧ ってな感じで、「マルチレコードインサート」ってのは「マルチプルインサート」のことを言ってるんだとは思いますが、
バルクインサートとマルチプルインサートが今頃別物だと気付いたのでまとめる。ググってても勘違いしてるひと多い。
⇧ 「バルクインサート」と「マルチプルインサート」が別物だったとは、確かに勘違いしておりました。
まぁ、というわけで、「SQLで頑張る」方法で複数テーブルに一度の処理でデータをINSERTするのは、データベースによっては実現できませんと、と言うか、2020年8月23日(日)時点だと、
の2つで実現できる可能性があるみたい、MySQLについては微妙なとこらしいですが...。
実際の現場だとどうしてるのか知りたいところですわ。
気になったのは、
「マルチテーブル・インサート文のどの部分にも順序を指定することはできません。」とか制限もあるし……
マルチテーブル・インサート文のどの部分にも順序を指定することはできません。マルチテーブル・インサートは、単一のSQL文とみなされます。したがって、NEXTVAL
を初めて参照するときに、その次の番号が生成された後、この番号と同じ番号が、この文の後続のすべての参照で戻されます。
https://docs.oracle.com/cd/E57425_01/121/SQLRF/statements_9015.htm#i2125362
⇧ ってな感じで、「マルチテーブル・インサート文のどの部分にも順序を指定することはできません。」って言葉の意味がいまいち伝わってこないんだが、もし上記が、各INSERT文を実行する順番が指定できない、ってことを言ってるとすると、「親」「子」関係のあるテーブル(「外部キー制約」のあるテーブル)でマルチテーブル・インサート使えないやん...
ってなると、複数回クエリを実行するか、DDDの集約を止めるか、って選択になってくるんですかね?
まじか...
やっぱり、Java を取り巻く環境っていろいろ破綻してるように思えてしかたない...
2020年8月23日(日)追記:↑ ここまで
2020年8月27日(木)追記:↓ ここから
やはりと言うか、
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.
⇧ マルチテーブル・インサートでのINSERTの順番が保障されないんだとか(涙)。
foreign key で親子関係になってしまってるテーブルでマルチテーブル・インサートはできないっちゅうことですかね...
2020年8月27日(木)追記:↑ ここまで
今回はこのへんで。