環境

  • GlassFish 4.1
  • JDK 1.8

課題

こんなテーブルがあったとします(太字は主キーです)。

HUMAN(ID)
CELL(CELL_ID, HUMAN_ID)

HUMAN対CELLは1:nの関係です。

CELL.HUMAN_IDが外部キーとなっていてHUMAN.IDを参照しています。

エンティティは2つになります。

ここで、CELL.HUMAN_IDにHUMAN.IDの値を自動でぶっこみたいなぁというお話です。

試行

試行1

@Idアノテーションを複数つけて、@IdClassで複合主キーを指定します。

Human.java

@Entity
public class Human implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Integer id;

    @Id
    @OneToMany(mappedBy = "human", cascade = CascadeType.ALL)
    private List<Cell> cellList;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
    
    public void addCell(Cell cell) {
        if (cell.getHuman() != this) {
            cell.setHuman(this);
        }
        if (cellList == null ){
            cellList = new ArrayList<>();
        }
        
        if (!cellList.contains(cell))  {
            cellList.add(cell);
        }
    }
    // 略 hashCodeやequals
}

Cell.java

@Entity
@IdClass(CellPK.class)
public class Cell implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "CELL_ID")
    private Integer id;

    @Id
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "HUMAN_ID", referencedColumnName = "ID")
    private Human human;
    // 略
}

CellPK.java

public class CellPK implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id;
    private Human human;
    // 略
}

結果

ここで実行すると、そもそもデプロイできないです。

Severe:   Exception while invoking class org.glassfish.persistence.jpa.JPADeployer prepare method
Severe:   javax.persistence.PersistenceException: Exception [EclipseLink-28018] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.EntityManagerSetupException
Exception Description: Predeployment of PersistenceUnit [jpa-composite-onetomany_war_1.0-SNAPSHOTPU] failed.
Internal Exception: Exception [EclipseLink-7332] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.ValidationException
Exception Description: The derived composite primary key attribute [human] of type [jpa.composite.onetomany.Human] from [jpa.composite.onetomany.CellPK] should be of the same type as its parent id field from [jpa.composite.onetomany.Human]. That is, it should be of type [java.lang.Integer].
	at org.eclipse.persistence.internal.jpa.EntityManagerSetupImpl.createPredeployFailedPersistenceException(EntityManagerSetupImpl.java:1954)
	at org.eclipse.persistence.internal.jpa.EntityManagerSetupImpl.predeploy(EntityManagerSetupImpl.java:1945)

型を合わせないといけないらしい。

試行2

型をあわせて、@JoinColumn@PrimaryKeyJoinColumnへ変更しました。@JoinColumnのままだと、更新できるフィールドが複数あるからと怒られます。

CellPK.java

public class CellPK implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id;
    private Integer humanId;
    // 略
}

Cell.java

@Entity
@IdClass(CellPK.class)
public class Cell implements Serializable {
    // 略
    @Id
    @Column(name = "HUMAN_ID")
    private Integer humanId;
    
    @ManyToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn(name = "HUMAN_ID", referencedColumnName = "ID")
    private Human human;
    // 略
}

結果

CELL.HUMAN_IDがセットされないので、SQLエラーになりました。

javax.persistence.PersistenceException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'HUMAN_ID' cannot be null
Error Code: 1048
Call: INSERT INTO CELL (HUMAN_ID, CELL_ID) VALUES (?, ?)
	bind => [2 parameters bound]
Query: InsertObjectQuery(jpa.composite.onetomany.Cell[ id=2 ])

試行3

Cellエンティティのセッターの中で、humanIdをセットするように変えてみました。

Cell.java

@Entity
@IdClass(CellPK.class)
public class Cell implements Serializable {
    // 略
    public void setHuman(Human human) {
        this.human = human;
        this.humanId = human.getId();
        human.addCell(this);
    }
    // 略
}

結果

状況変わらず……。

Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'HUMAN_ID' cannot be null
Error Code: 1048
Call: INSERT INTO CELL (HUMAN_ID, CELL_ID) VALUES (?, ?)
	bind => [2 parameters bound]
Query: InsertObjectQuery(jpa.composite.onetomany.Cell[ id=2 ])

試行4

そもそも呼び方間違ってね?


        Human human = new Human();
        human.addCell(new Cell());
        human.addCell(new Cell());
        em.persist(human);        

を、


        Human human = new Human();
        em.persist(human);   
        human.addCell(new Cell());
        human.addCell(new Cell());

のように書き換えました。emEntityManagerです。

結果

成功しました~~~(喜)

@OneToOneなら関連テーブルもいっぺんにpersistできるけど、@OneToManyは先にpersistしないといけないみたい。

結論

OneToManyを保存するときは、先に親エンティティをpersistすべし。