今回のペイン(≒日記帳)
- DTO中の
List<? extends Enum>
のようなフィールドを CSV でなくちゃんと正規化したい。 - Entity で
List<ChildEntity>
等として持った場合に、当該ChildEntity にサロゲートキーを付けると、update 操作のたびに毎回サロゲートキーを取りに行くか、さもなくば ChildEntity のレコードを全件 insert し直すことになる。- 後者の場合、親への
update 操作の度に子の ID が飛んで気持ち悪い。(実害はあまりない気がするが)
- 後者の場合、親への
=> そうだ複合主キーを使おう
テーブル例
MySQL なら例えばこんなテーブル
CREATE TABLE parent(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
bar VARCHAR(30) NOT NULL
);
CREATE TABLE parent_child(
parent_id INT NOT NULL,
value_ VARCHAR(30) NOT NULL,
PRIMARY KEY(parent_id, value_),
FOREIGN KEY(parent_id) REFERENCES parent(id)
);
ChildEntity
複合主キーなので @EmbeddedId
を使いますが、@ManyToOne
を付ける親エンティティへの参照は PKクラス ではなく 外側の Entityクラスのフィールドにします。1
@ManyToOne
と合わせて @MapsId
を付すことで、親子同時に insert したときによろしく親テーブル側の ID を振ってくれます。が、親 update 子 insert の場合は面倒を見てくれないので、自分で parent と parentId を同期する必要があります。
@Entity
@Table(name = "parent_child")
@ToString(exclude = "parent")
@NoArgsConstructor
public class ChildEntity {
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class PK implements Serializable {
@Column(name = "parent_id")
private int parentId;
@Column(name = "value_")
private String value;
}
@EmbeddedId
@Getter
@Setter
private PK pk;
@ManyToOne
@JoinColumn(name = "parent_id", referencedColumnName = "id")
@MapsId("parentId")
@Getter
@Setter
private ParentEntity parent;
public ChildEntity(@NonNull final ParentEntity parent,
@NonNull final String value) {
this.pk = new PK(parent.getId(), value);
this.parent = parent;
}
public void setParent(@NonNull final ParentEntity parent) {
this.pk.setParentId(parent.getId());
this.parent = parent;
}
}
親子で循環参照になっているため、lombok.ToString や lombok.EqualsAndHashCode、Jackson 等を不用意に使うと無限ループを引き起こすことに注意。
ParentEntity
親側は通常の(サロゲートキーを使う場合の) @OneToMany
と変わりません。
@Entity
@Table(name = "parent")
@Data
public class ParentEntity {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@OneToMany(mappedBy = "parent", orphanRemoval = true, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch(FetchMode.SUBSELECT)
private List<ChildEntity> children;
@Column(name = "bar")
private String bar;
}
@OneToMany
のパラメータに orphanRemoval をセットしておかないと親側の children を消しても子のレコードが消えないので注意。
また、@Fetch(FetchMode.SUBSELECT)
をつけないとN+1問題が発生します。
更新時に発行されるSQL
Hibernate 5.0.12 だとこんな感じ。
新しく追加した ChildEntity を insert する際に、ParentEntity と一緒に取得した List とは別に一件一件 select している点が気になります。
変更のない要素については EAGER フェッチで済んでいるので、今回の案件では問題ないかなというところ。
select parententi0_.id as id1_0_0_, parententi0_.bar as bar2_0_0_, children1_.parent_id as parent_i1_1_1_, children1_.value_ as value_2_1_1_, children1_.parent_id as parent_i1_1_2_, children1_.value_ as value_2_1_2_ from parent parententi0_ left outer join parent_child children1_ on parententi0_.id=children1_.parent_id where parententi0_.id=?
binding parameter [1] as [INTEGER] - [5]
select childentit0_.parent_id as parent_i1_1_0_, childentit0_.value_ as value_2_1_0_ from parent_child childentit0_ where childentit0_.parent_id=? and childentit0_.value_=?
binding parameter [1] as [INTEGER] - [5]
binding parameter [2] as [VARCHAR] - [0.42733237750132513]
insert into parent_child (parent_id, value_) values (?, ?)
binding parameter [1] as [INTEGER] - [5]
binding parameter [2] as [VARCHAR] - [0.42733237750132513]
delete from parent_child where parent_id=? and value_=?
binding parameter [1] as [INTEGER] - [5]
binding parameter [2] as [VARCHAR] - [0.8694858141499555]
-
@ManyToOne
用のフィールドをPKクラスに持つと read 操作時に StackOverflowError が発生します。 ↩