Posted under JPA
Permalink
Tags Eclipselink, Java, JPA, Tip, Tutorial
All my entities have a numeric ID as a primary key, except in very exceptional cases. In particular, I never used business data as part of the primary key, nor do I ever use a primary key for any business purposes (such as an invoice number). I consider that primary and foreign keys are only used to define relationships, i.e. they are metadata as far as the domain model is concerned.
The problem of using business data in primary keys is that if the business data format for a primary key changes in any way, all the foreign keys referring to this primary key column must also be changed. This can result in massive changes throughout the data model for what started as a simple business change. Taking the above invoice number for example, the business may decide to add an alphanumeric branch prefix to the invoice number.
If instead all primary keys are kept as separate numeric IDs, changes to the format of business data should typically only require changes to one table.
With that rant out of the way, the main point of this post is that, given the above, plus the need to hold version numbers on all entities to support optimistic locking, it is desirable to hold all the primary keys and version numbers in a mapped abstract superclass. This has the following benefits:-
- the entities are simplified and the key/version number implementation is encapsulated and centralised, which is a good design principle
- the mechanism allows for a standard mechanism to get the primary key/version of any entity polymorphically via the mapped superclass. This is especially useful when you need to do comparisons between entities and you do not want to override equals on the entities. It is also useful for other polymorphic situations – in my case I also have a polymorphic tree implementation which makes use of this
Note the following points about the implementation:-
- As I have discussed here, I always annotate getters rather than fields, to permit inheritance. In this case, I provide fields plus polymorphic getters/setters in the mapped superclass for getEntityId and setEntityId which are marked transient and are not annotated. They are purely to allow polymorphic access to the primary key.
- Each entity defines its own getters/setters with a mnemonic name for the primary key. These getters/setters call the base polymorphic ones. The reason for this is that as well as improving the key naming, it allows each entity to annotate its primary key differently, for example to use a specific generator for sequences etc.
- As the primary keys are annotated in the subclasses, this permits the base superclass IDs to be generic. In my case, this caters for both Long and Integer primary keys using a generic base superclass. Note that it would not be possible to annotate in the superclass if the ID is a parameterised type – JPA spits this out and will not allow it.
- Due to the above generic issue, the type for the version number must be fixed, as this is annotated in the superclass. In my case I settled for an int for version numbers.
- Note that as the primary key is generic, it must therefore be a reference type rather than a primitive type (e.g. Long rather than long). In my case, primary keys are normally Long
- In some cases, I have other mapped superclasses, such as Tree and TreeNode classes. In this case, Tree and TreeNode extend the EntityBase class, and Tree and TreeNode subclasses then just extend Tree and TreeNode respectively.
The following code illustrates a simple example of the superclass and an entity subclass:-
EntityBase
/**
* Entity implementation class for Entity: EntityBase
* This mapped superclass defines the primary key and locking version number for all entities.
* The getters/setters are marked transient so they are not mapped here. This allows polymorphic access to primary key (and version),
* but still allows the subclass to define its own (mapped) getter/setter names for the primary key, and to annotate for specific generators etc.
* This allows meaningful names for primary keys, normally <tableName>Id
*/
@MappedSuperclass
@Access(AccessType.PROPERTY)
public abstract class EntityBase<C> implements Serializable {
private static final long serialVersionUID = 8075127067609241095L;private C entityId;
private int version;
@Transient public C getEntityId() {
return entityId;
}
@Transient public void setEntityId(C entityId) {
this.entityId = entityId;
}
/*
* The version cannot be generic as it is annotated here (unlike the primary key)
* If you do, Eclipselink throws a tantrum. In practice, an int is plenty –
* it would allow continuous version changes once a second for over 68 years for example.
*/
@Version
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
}
Address
@Entity
@Access(AccessType.PROPERTY)
public class Address extends EntityBase<Long> implements Serializable {
private static final long serialVersionUID = 3206799789679218177L;
private int latitude;
private int longitude;
private String streetAddress1;
private String streetAddress2;
private String locality;
private String postTown;
private String county;
private String postCode;
public Address(){}@Id
@GeneratedValue(generator="AddressId")
public Long getAddressId() {return super.getEntityId();}
public void setAddressId(Long addressId) {super.setEntityId(addressId);}
public int getLatitude() {return latitude;}
public void setLatitude(int latitude) {this.latitude = latitude;}
public int getLongitude() {return longitude;}
public void setLongitude(int longitude) {this.longitude = longitude;}
public String getStreetAddress1() {return streetAddress1;}
public void setStreetAddress1(String streetAddress1) {this.streetAddress1 = streetAddress1;}
public String getStreetAddress2() {return streetAddress2;}
public void setStreetAddress2(String streetAddress2) {this.streetAddress2 = streetAddress2;}
public String getLocality() {return locality;}
public void setLocality(String locality) {this.locality = locality;}
public String getPostTown() {return postTown;}
public void setPostTown(String postTown) {this.postTown = postTown;}
public String getCounty() {return county;}
public void setCounty(String county) {this.county = county;}
public String getPostCode() {return postCode;}
public void setPostCode(String postCode) {this.postCode = postCode;}
}
Leave a Reply
You must be logged in to post a comment.