1
votes

I have the following database tables:

  • party, with a pk "pty_id" connected to a sequence for generating pk values.
  • person with a fpk "prs_pty_id" in an identifying relation to party.pty_id.
  • company ... which is not involved at the moment, but obviously this is kindof sub-superclass setup, and it could probably have been implemented with the subclassing mechanism in postgresql, but that's for another day.

So, I use Netbeans 6.9.1 to generate JPA entity classes and controller/dao code to handle this. It works just nice, I only have to add one annotation to the Party Entity bean: @GeneratedValue(strategy = GenerationType.IDENTITY). This is not needed for the Person entity bean, because it should always have the pk value of the Party that it is connected to.

So here is what I do to create a person:

PartyJpaController parController = new PartyJpaController();
PersonJpaController perController = new PersonJpaController();
Party par = new Party();
Person per = new Person();
par.setComment("jalla");
per.setName("Per Vers");
parController.create(par);
per.setPrsPtyId(par.getPtyId()); // <== why do I need to set this ...
Long partyId = par.getPtyId();
par.setPerson(per); // <== ... when this explicitly expresses the relationship?
perController.create(per);
parController.edit(par);

Party foundParty = parController.findParty(partyId);

Person foundPerson = foundParty.getPerson();
System.err.println(foundPerson.getName());

This works just fine. But why do I have to explicitly set the pk of the Person bean? It is in an identifying relationship with the Party. If I skip it, I get

java.lang.IllegalArgumentException: An instance of a null PK has been incorrectly provided for this find operation.

in perController.create(per), which is code generated by Netbeans:

EntityManager em = null;
try {
    em = getEntityManager();
    em.getTransaction().begin();
    Party party = person.getParty();
    if (party != null) {
        party = em.getReference(party.getClass(), party.getPtyId()); // <== Exception thrown here
        person.setParty(party);
    }
    em.persist(person);
    if (party != null) {
        party.setPerson(person);
        party = em.merge(party);
    }
    em.getTransaction().commit();

So, I suppose the Netbeans-generated code is not quite tuned for identifying relationships? What's the best way to code this?

Software used: Eclipselink version 2.1.1 Postgresql 8.4 Netbeans 6.9.1 Java/JDK 1.6.0_21

Here are my beans, they're generated by netbeans 6.9.1 from schema, except the @GeneratedValue(strategy = GenerationType.IDENTITY) in Party, which I've added in order to use the serial/sequence pk generation in postgresql.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.martinsolaas.webmarin.jpa;

import java.io.Serializable;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapsId;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

/**
 *
 * @author jms
 */
@Entity
@Table(name = "person", catalog = "webmarin", schema = "webmarin")
@NamedQueries({
    @NamedQuery(name = "Person.findAll", query = "SELECT p FROM Person p"),
    @NamedQuery(name = "Person.findByPrsPtyId", query = "SELECT p FROM Person p WHERE p.prsPtyId = :prsPtyId"),
    @NamedQuery(name = "Person.findByName", query = "SELECT p FROM Person p WHERE p.name = :name"),
    @NamedQuery(name = "Person.findByCellphone", query = "SELECT p FROM Person p WHERE p.cellphone = :cellphone"),
    @NamedQuery(name = "Person.findByOfficephone", query = "SELECT p FROM Person p WHERE p.officephone = :officephone")})
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @Column(name = "prs_pty_id", nullable = false)
    @MapsId
    private Long prsPtyId;
    @Column(name = "name", length = 255)
    private String name;
    @Column(name = "cellphone", length = 55)
    private String cellphone;
    @Column(name = "officephone", length = 55)
    private String officephone;
    @JoinColumn(name = "prs_pty_id", referencedColumnName = "pty_id", nullable = false, insertable = false, updatable = false)
    @OneToOne(optional = false)
    private Party party;

    public Person() {
    }

    public Person(Long prsPtyId) {
        this.prsPtyId = prsPtyId;
    }

    public Long getPrsPtyId() {
        return prsPtyId;
    }

    public void setPrsPtyId(Long prsPtyId) {
        this.prsPtyId = prsPtyId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCellphone() {
        return cellphone;
    }

    public void setCellphone(String cellphone) {
        this.cellphone = cellphone;
    }

    public String getOfficephone() {
        return officephone;
    }

    public void setOfficephone(String officephone) {
        this.officephone = officephone;
    }

    public Party getParty() {
        return party;
    }

    public void setParty(Party party) {
        this.party = party;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (prsPtyId != null ? prsPtyId.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Person)) {
            return false;
        }
        Person other = (Person) object;
        if ((this.prsPtyId == null && other.prsPtyId != null) || (this.prsPtyId != null && !this.prsPtyId.equals(other.prsPtyId))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "com.martinsolaas.webmarin.jpa.Person[prsPtyId=" + prsPtyId + "]";
    }

}

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.martinsolaas.webmarin.jpa;

import java.io.Serializable;
import java.util.List;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.MapsId;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToOne;
import javax.persistence.Table;

/**
 *
 * @author jms
 */
@Entity
@Table(name = "party", catalog = "webmarin", schema = "webmarin")
@NamedQueries({
    @NamedQuery(name = "Party.findAll", query = "SELECT p FROM Party p"),
    @NamedQuery(name = "Party.findByPtyId", query = "SELECT p FROM Party p WHERE p.ptyId = :ptyId"),
    @NamedQuery(name = "Party.findByComment", query = "SELECT p FROM Party p WHERE p.comment = :comment")})
public class Party implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @Column(name = "pty_id", nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long ptyId;
    @Basic(optional = false)
    @Column(name = "comment", nullable = false, length = 2147483647)
    private String comment;
    @JoinTable(name = "party_relationship", joinColumns = {
        @JoinColumn(name = "parent_pty_id", referencedColumnName = "pty_id", nullable = false)}, inverseJoinColumns = {
        @JoinColumn(name = "child_pty_id", referencedColumnName = "pty_id", nullable = false)})
    @ManyToMany
    private List partyList;
    @ManyToMany(mappedBy = "partyList")
    private List partyList1;

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "party")
    private Person person;

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "party")
    private Company company;

    public Party() {
    }

    public Party(Long ptyId) {
        this.ptyId = ptyId;
    }

    public Party(Long ptyId, String comment) {
        this.ptyId = ptyId;
        this.comment = comment;
    }

    public Long getPtyId() {
        return ptyId;
    }

    public void setPtyId(Long ptyId) {
        this.ptyId = ptyId;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public List getPartyList() {
        return partyList;
    }

    public void setPartyList(List partyList) {
        this.partyList = partyList;
    }

    public List getPartyList1() {
        return partyList1;
    }

    public void setPartyList1(List partyList1) {
        this.partyList1 = partyList1;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    public Company getCompany() {
        return company;
    }

    public void setCompany(Company company) {
        this.company = company;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (ptyId != null ? ptyId.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof Party)) {
            return false;
        }
        Party other = (Party) object;
        if ((this.ptyId == null && other.ptyId != null) || (this.ptyId != null && !this.ptyId.equals(other.ptyId))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "com.martinsolaas.webmarin.jpa.Party[ptyId=" + ptyId + "]";
    }

}

Eventually, here is the schema SQL


CREATE SEQUENCE webmarin.party_pty_id_seq;

CREATE TABLE webmarin.party (
                pty_id BIGINT NOT NULL DEFAULT nextval('webmarin.party_pty_id_seq'),
                comment TEXT NOT NULL,
                CONSTRAINT pty_pk PRIMARY KEY (pty_id)
);


ALTER SEQUENCE webmarin.party_pty_id_seq OWNED BY webmarin.party.pty_id;

CREATE TABLE webmarin.company (
                cmp_pty_id BIGINT NOT NULL,
                name VARCHAR(255) NOT NULL,
                CONSTRAINT cmp_pk PRIMARY KEY (cmp_pty_id)
);


CREATE TABLE webmarin.party_relationship (
                parent_pty_id BIGINT NOT NULL,
                child_pty_id BIGINT NOT NULL,
                CONSTRAINT ptr_pk PRIMARY KEY (parent_pty_id, child_pty_id)
);


CREATE TABLE webmarin.person (
                prs_pty_id BIGINT NOT NULL,
                name VARCHAR(255),
                cellphone VARCHAR(55),
                officephone VARCHAR(55),
                CONSTRAINT prs_pk PRIMARY KEY (prs_pty_id)
);


ALTER TABLE webmarin.party_relationship ADD CONSTRAINT parent_party_party_relationship_fk
FOREIGN KEY (parent_pty_id)
REFERENCES webmarin.party (pty_id)
ON DELETE NO ACTION
ON UPDATE NO ACTION
NOT DEFERRABLE;

ALTER TABLE webmarin.party_relationship ADD CONSTRAINT child_party_party_relationship_fk
FOREIGN KEY (child_pty_id)
REFERENCES webmarin.party (pty_id)
ON DELETE NO ACTION
ON UPDATE NO ACTION
NOT DEFERRABLE;

ALTER TABLE webmarin.person ADD CONSTRAINT party_person_fk
FOREIGN KEY (prs_pty_id)
REFERENCES webmarin.party (pty_id)
ON DELETE NO ACTION
ON UPDATE NO ACTION
NOT DEFERRABLE;

ALTER TABLE webmarin.company ADD CONSTRAINT party_company_fk
FOREIGN KEY (cmp_pty_id)
REFERENCES webmarin.party (pty_id)
ON DELETE NO ACTION
ON UPDATE NO ACTION
NOT DEFERRABLE;
1

1 Answers

0
votes

JPA 2.0 improved considerably the support of derived identifiers with the Id and MapsId annotations (they should be preferred now) that you can use on XxxToOne associations. Below one of the example taken from the specification:

2.4.1.3 Examples of Derived Identities

...

Example 4:

The parent entity has a simple primary key:

@Entity
public class Person {
    @Id String ssn;
    ...
}

Case (a): The dependent entity has a single primary key attribute which is mapped by the relationship attribute. The primary key of MedicalHistory is of type String.

@Entity
public class MedicalHistory {
    // default join column name is overridden
    @Id
    @OneToOne
    @JoinColumn(name="FK")
    Person patient;
    ...
}

Sample query:

SELECT m
FROM MedicalHistory m
WHERE m.patient.ssn = '123-45-6789'

Case (b): The dependent entity has a single primary key attribute corresponding to the relationship attribute. The primary key attribute is of the same basic type as the primary key of the parent entity. The MapsId annotation applied to the relationship attribute indicates that the primary key is mapped by the relationship attribute.

@Entity
public class MedicalHistory {
    @Id String id; // overriding not allowed
    ...
    // default join column name is overridden
    @MapsId
    @JoinColumn(name="FK")
    @OneToOne Person patient;
    ...
}

Sample query:

SELECT m
FROM MedicalHistory m WHERE m.patient.ssn = '123-45-6789'

I think that the MapsId might be what you're looking for.

Follow-up: Instead of this:

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @Column(name = "prs_pty_id", nullable = false)
    @MapsId
    private Long prsPtyId;
    @Column(name = "name", length = 255)
    private String name;
    @Column(name = "cellphone", length = 55)
    private String cellphone;
    @Column(name = "officephone", length = 55)
    private String officephone;
    @JoinColumn(name = "prs_pty_id", referencedColumnName = "pty_id", nullable = false, insertable = false, updatable = false)
    @OneToOne(optional = false)
    private Party party;
    ...
}

Try this:

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @Column(name = "prs_pty_id", nullable = false)
    private Long prsPtyId;
    @Column(name = "name", length = 255)
    private String name;
    @Column(name = "cellphone", length = 55)
    private String cellphone;
    @Column(name = "officephone", length = 55)
    private String officephone;
    @MapsId
    @JoinColumn(name = "prs_pty_id", referencedColumnName = "pty_id")
    @OneToOne
    private Party party;
    ...
}

References

  • JPA 2.0 specification
    • Section 2.4.1 "Primary Keys Corresponding to Derived Identities"
    • Section 2.4.1.1 "Specification of Derived Identities"
    • Section 2.4.1.2 "Mapping of Derived Identities"
    • Section 2.4.1.3 "Examples of Derived Identities"
  • JPA Wikibook