Hibernate JPA で複合主キーの一部に @ManyToOne を付ける

# 今回のペイン（≒日記帳）
* DTO中の `List<? extends Enum>` のようなフィールドを CSV でなくちゃんと正規化したい。
* Entity で `List<ChildEntity>` 等として持った場合に、当該ChildEntity にサロゲートキーを付けると、update 操作のたびに毎回サロゲートキーを取りに行くか、さもなくば ChildEntity のレコードを全件 insert し直すことになる。
    * 後者の場合、親への
 update 操作の度に子の ID が飛んで気持ち悪い。（実害はあまりない気がするが）

=> そうだ複合主キーを使おう

# テーブル例
MySQL なら例えばこんなテーブル

```sql
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]
[^1]: `@ManyToOne` 用のフィールドをPKクラスに持つと read 操作時に StackOverflowError が発生します。

`@ManyToOne` と合わせて `@MapsId` を付すことで、親子同時に insert したときによろしく親テーブル側の ID を振ってくれます。が、親 update 子 insert の場合は面倒を見てくれないので、自分で parent と parentId を同期する必要があります。

```java:ChildEntity.java
@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` と変わりません。

```java:ParentEntity.java
@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 フェッチで済んでいるので、今回の案件では問題ないかなというところ。

```sql
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]
```