2
votes

I have a parent table i.e. audit_log(parent) which contains a single column id. For a given id in audit_log, I have a list of vendor ID's. I am storing them in separate table audit_log_vendorid(child table). I want the child table to get the id from the parent table as one of the columns(parent_id). Here's the table schema.

audit_log

+-------+------------+------+-----+---------+-------+
| Field | Type       | Null | Key | Default | Extra |
+-------+------------+------+-----+---------+-------+
| id    | bigint(19) | NO   | PRI | NULL    |       |
+-------+------------+------+-----+---------+-------+

audit_log_vendorid

+-----------+------------+------+-----+---------+----------------+
| Field     | Type       | Null | Key | Default | Extra          |
+-----------+------------+------+-----+---------+----------------+
| id        | bigint(19) | NO   | PRI | NULL    | auto_increment |
| vendor_id | bigint(19) | NO   |     | NULL    |                |
| parent_id | bigint(19) | NO   |     | NULL    |                |
+-----------+------------+------+-----+---------+----------------+

I'have defined my hibernate classes as follows

@Entity
@Table(name="audit_log")
public class AuditLog  {

private List<AuditVendorPair> vendorIDs;


public AuditLog(List<AuditVendorPair> vendorIds) throws Exception {
    this.vendorIDs = vendorIDs;
}

@OneToMany(cascade=CascadeType.ALL)
@JoinTable(name = "audit_log_vendorid",  
   joinColumns = { @JoinColumn(name = "parent_id", referencedColumnName="id") })
public List<AuditVendorPair> getVendors() {
    return vendorIDs;
}

@Id @Column(name="ID")
public Long getId() {
    return super.getId();
}

public void setHostServices(List<AuditVendorPair> vendorIDs){
    this.vendorIDs = vendorIDs;
}

}

My hibernate mapping class for audit_log_vendorid is below. I pass in a vendor id and expect the other two fields to be populated by hibernate. The parent_id field I want from "id" field in audit_log. It's initialized as null as of now causing mysql constraint exception.

@Entity
@Table(name="audit_log_vendorid")
public class AuditVendorPair {

private Long id;
private Long parent_id;
private Long vendor_id;
public AuditVendorPair(Long vendor_id){
    this.vendor_id = vendor_id;
}

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
public Long getId(){
    return id;
}

public void setId(Long id){
    this.id = id;
}

@Column(name="vendor_id")
public Long getVendorID() {
    return vendor_id;
}

public void setVendorID(Long vendor_id){
    this.vendor_id = vendor_id;
}


@Column(name="parent_id")
public Long getParentId() {
    return parent_id;
}

public void setParentId(Long parentID){
    this.parent_id = parentID;
}
}

I am curious to know if my annotations are correct. I basically want the id from the audit_log table to be populated in the parent_id field in audit_log_vendorid table by hibernate.

3

3 Answers

1
votes

No, they're not correct. audit_log_vendorid is not a join table. A join table is a table that is not mapped to an entity, and which contains IDs of two associated entities, mapped to other tables.

You also shouldn't have a parent_id field in the AuditVendorPair. Not only because that doesn't respect the Java naming conventions, but also because it should be replaced by a reference to AuditLog, mapped with ManyToOne.

So, in short, you should have a bidirectional OneToMany association, mapped as explained in the documentation.

1
votes

I think you are overlooking a key concept in JPA, which is entities are objects, so you would never have an entity referring to it's parent using the ID directly, you would refer to the object (JPA will use the id when querying the DB)

@Entity
@Table(name="audit_log")
public class AuditLog  {

    @OneToMany(cascade= CascadeType.ALL, mappedBy = "auditLog")
    private Collection<AuditVendorPair> vendorIDs;

    @Id @Column(name="id")
    private Long id;

    public AuditLog() {
    }

    public Collection<AuditVendorPair> getVendors() {
        if (vendorIDs == null) {
            vendorIDs = new ArrayList<>();
        }
        return vendorIDs;
    }

    public long getId() {
        return id;
    }
}

and

@Entity
@Table(name = "audit_log_vendorid")
public class AuditVendorPair {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @JoinColumn(nullable = false, name = "parent_id")
    @ManyToOne(optional = false)
    private AuditLog auditLog;

    @Column(name = "vendor_id")
    private Long vendorId;

    public AuditVendorPair() {
    }

    public long getVendorId() {
        return vendorId;
    }

    public void setVendorId(long vendorId) {
        this.vendorId = vendorId;
    }

    public AuditLog getAuditLog() {
        return auditLog;
    }

    public void setAuditLog(AuditLog auditLog) {
        this.auditLog = auditLog;
    }
}

AuditVendorPair is referring to AuditLog using the Entity, and since this is an entity relation, you have to use @JoinColumn to specify the name.

Some best practices for JPA/Hibernate

  • No args constructor is required by the spec.
  • Don't have a setter for the primary key, unless you generate it in code.
  • You can't use primitives for fields. However if the database enforces NOT NULL, you should use primitive long in getters and setters, so your IDE can warn you about NPE, instead of waiting for your tests to fail.
  • Use Collection not List, as list implies order.
  • Done use underscores in field names, but use them when specifying the name of the column.
  • Notice the null check and collection creation in getVendors(), this is to avoid NPE when the object is first created. This means that you can use the same logic for adding a AuditVendorPair regardless if the AuditLog has just been created, or if it is loaded from the DB. It also means I don't create a setVendors() unless I need to replace the entire list, which is rarely the case, and when it is, you typically need to explicitly delete every entity in the list.
0
votes

It sounds like you should read a little more about the principles behind JPA. Here is a 'raw' JPA example of what the code will look like, if you use Spring-data and autowire @PersistanceUnit you don't need to manager the entityManager youself.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("PU-Name");
EntityManager em = emf.createEntityManager();

try {
    long primaryKey = 1L; // comes from somewhere else
    long vendorId = 1L;   // comes from somewhere
    AuditLog log = em.find(AuditLog.class, primaryKey); // loaded from DB

    AuditVendorPair pair = new AuditVendorPair();
    pair.setAuditLog(log);
    pair.setVendorId(vendorId);
    em.getTransaction().begin();
    em.persist(pair);
    em.getTransaction().commit();
} finally {
    em.close();
}

You have to connect the entities in code, and any existing entities have to be loaded from the database. If you are storing a new AuditVendorPair, you first have to load the AuditLog object, and then set it on the newly created AuditVendorPair. Typically Vendor will also be an Entity, so it will also have to be looked up.

Note: The example above does not maintain the two-way relationship between AuditLog and AuditVendorPair, since AuditVendorPair is not added to the collections of vendors on AuditLog. Since the column defining the relation is on AuditVendorPair (which is stored) this is not an issue, because next time you load the AuditLog instance from the DB the AuditVendorPair will be part of the vendor collection. However if you use the AuditLog after the persistence context is closed, you may want to maintain the two-way relation.

Note: the example above assumes that you get requests with primary keys. In general you should never expose primary keys in any front-end, in our systems we generate a unique field (UUID) for every entity that is exposed in a UI.