EntityオブジェクトのIdentifier
DDDのEntityクラスに分類されるオブジェクトは、属性としてIdentifierを持つが、これは新規にオブジェクトが作られて永続化されるまでは、Identifierを与えられないがどうするか? という問題。
JPAのケース
以下のようにIdentifierには@Idアノテーションを付与し、JPAで自動採番してほしい場合にはさらに@GeneratedValueをつける。
code:User.java
@Entity
@Data
class User {
@Id
@GeneratedValue
Long id;
String name;
}
Userオブジェクトを作るときには、idはnullにしておき、永続化するとそれがJPA側で採番した値で更新されるという仕組み。
code:UserRepository.java
@Component
class UserRepository {
@AutoWired
private UserRepository userRepository;
void save(User user) {
// user.getId() ==> null
userRepository.save(user);
// user.getId() ==> 1L
}
}
典型的なORマッパーは、こんな挙動である。
Identifierをどこで生成するか?
Implementing Domain-Driven Designでは、永続化層でID生成せずとも、ドメイン層またはその前段で作るやり方もあるよ、としている。Natural Keyで無い限り、永続化層でないところで一意性担保するには使えるID生成アルゴリズムはそう多くないが、UUIDやGUIDがよく使われているようである。
Hands on Domain Driven Design with .NET Core
DDD by Example
GitHubのDDD Example的なリポジトリをいくつか調査する限り、ほとんどがこの永続化層以外で生成するパターンであった。
UUIDやGUIDをEntityのIdentifierにするのは簡単だが、文字列表現にすると長すぎるので、特にこだわりがなければNano IDを使った方がよいだろう。
それでも永続化層でID生成しなきゃいけない場合
以下2点をちょっとした前提としておく。
PersistenceレイヤとDomainレイヤは分離する
後述
Entityクラスも出来るだけイミュータブルに作りたい
EntityクラスはIdentityで識別される状態を表す
永続化前と永続化後で別の型にする
未採番を表現できるIdentity型を定義する
DatomicのtempIdは、イミュータブルなデータ型しか存在しないClojureで、このような永続化層でのIDを生成を扱うのに考えられた仕組みである。
同様のことを、Javaで実装するには、以下のようなResolvableIdを用意しておくと良いだろう。
code:ResolvableId.java
public abstract class ResolvableId<T> {
T value;
protected ResolvableId() {
this.value = null;
}
public ResolvableId(T value) {
if (value == null) {
throw new IllegalArgumentException("value must not be null");
}
this.value = value;
}
public boolean isResolved() {
return value != null;
}
public void resolve(T value) {
if (this.value != null) {
throw new IllegalStateException("ID has already resolved");
}
this.value = value;
}
public T getValue() {
if (value == null) {
throw new UnsupportedOperationException("");
}
return value;
}
@Override
public boolean equals(Object o) {
return Optional.ofNullable(o)
.filter(ObId.class::isInstance)
.map(ObId.class::cast)
.filter(ObId::isResolved)
.map(obId -> obId.value.equals(this.value))
.orElse(false);
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
@Override
public String toString() {
return Objects.toString(value, "unresolved");
}
}
EntityのIdクラスはこれを実装継承して作る。
code:UserId.java
public class UserId extends ResolvableId<Long> {
private UserId() {
super();
}
public UserId(Long value) {
super(value);
}
public static UserId unresolved() {
return new UserId();
}
}
こうしておくと、永続化層でID生成した結果をUserIdに反映できる。UserIdは一度値をもつと再び別の値で更新することはできないし、Userクラスのidプロパティは、Read Onlyなので、IDが知らないところで書き換わってしまう事故も防げる。
code:java
User user = new User(UserId.unresolved, "user1");
// Persistence層で…
UserEntity userEntity = mapper.domainToEntity(user);
userRepository.save(userEntity);
user.getId().resolve(user.getId());
PersistenceレイヤとDomainレイヤを分離する
JPAのEntityは頑張ればドメインモデルとしても使えるが、相当頑張らないとデータベースのテーブル設計に引っ張られるので、通常はPersistenceレイヤ(Infrastractureレイヤ)のオブジェクトとして扱うのが取り回しがシンプルになる。したがって、それに対応するドメインレイヤのEntityクラス(用語が紛らわしいので、JPAのEntityは以後必ず「JPAの」を付けて呼ぶ)は別で用意する。