LoginSignup
3
3

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-10-26

今回のペイン(≒日記帳)

  • 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 を同期する必要があります。

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 と変わりません。

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

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]

  1. @ManyToOne 用のフィールドをPKクラスに持つと read 操作時に StackOverflowError が発生します。 

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3