Search This Blog

Sunday 24 March 2013

First Look at JPA Annotations

In the earlier posts we saw how to run a JPA application. I decided to have a look at the annotations used in the previous examples.
First the Owner class:
@Entity()
@Table(name = "OWNER",schema="firstOne")
public class Owner {
The first annotation that we find with every Java Entity is... Entity. (No surprise there). The annotation specifies that the class is an entity. The annotation includes a single attribute name. Any value provided here will appear in the SQL queries. For example if we tweak the above code:
@Entity(name="hero")
@Table(name = "OWNER",schema="firstOne")
public class Owner {
The JPA QL query to select a record will be :
Query query = entityManager
    .createQuery("SELECT owner FROM hero owner where owner.id = 1");
System.out.println(query.getSingleResult());
As can be seen the Entity name is not 'Owner' but 'hero'. Using 'Owner' now will result in an exception. The SQL query in both cases remains the same. The attribute impacts the way we refer the entity in JPA QL.
The next annotation is Table. It provides information of the table that the java class maps to. The name attribute specifies the table name. The second attribute is the schema attribute. A PostgreSQL database is composed of multiple schema with each schema consisting of tables. In databases like MySQL one database has one schema. So the attribute can be ignored there. The catalog attribute can be used to specify the catalog used by the database. The last attribute is the uniqueConstraints attribute. It takes an array of unique column constraints. For example in the Pet class:
@Table(name = "PET", schema="firstOne",
        uniqueConstraints = { @UniqueConstraint(columnNames = { "TAG_ID" }) })
In the DDL generated, a unique column constraint will be added to TAG_ID. All attributes of the Table annotation are optional.
The next in the code is the identifier column.The Id annotation indicates that the property is the primary key. It does not include any attributes. JPA assumes the primary column is named ID.
What if the primary column is under a different name ? How do we specify it ?
@Id
@Column(name = "P_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
    return id;
}
I modified the Owner class to include a primary key column with name P_ID. The DDL generated indicates the name of primary column :
    create table firstOne.OWNER (
        P_ID  bigserial not null,
        NAME varchar(255),
        USER_ID varchar(20) not null unique,
        primary key (P_ID)
    )
Also the earlier JPA query would now translate as:
    select
        owner0_.P_ID as P1_0_,
        owner0_.NAME as NAME0_,
        owner0_.USER_ID as USER3_0_ 
    from
        firstOne.OWNER owner0_ 
    where
        owner0_.P_ID=1 limit ?
The other annotation I used was the GeneratedValue annotation.It is used for the specification of the primary key generation strategy. In this class Identity generation strategy was used; - the strategy attribute being set to GenerationType.IDENTITY . All attributes are optional here.
The other annotation - one that occurs most commonly in the entities is the Column annotation. It can be used without any attributes. Here I have just added the name annotation. I shall look into those attributes in detail in the next post. The Owner class included a collection of pets.
@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE },
           mappedBy = "owner", fetch = FetchType.LAZY, targetEntity = Pet.class)
public Set<Pet> getPets() {
    return pets;
}
One Owner has many pets. Hence the @OneToMany annotation. The first attribute set is the cascade settings - here Pets will have their merge and save operations happen along with the Owner object. Other options include
  • CascadeType.REMOVE
  • CascadeType.REFRESH and
  • CascadeType.ALL
JPA does not include the cascade delete-orphan option supported by Hibernate. To use the same we would have to include a Hibernate annotation. With collections the inverse attribute cannot be far behind. JPA provides support for the same through the mappedBy attribute.In our example we added the value in the OneToMany annotation. mappedBy="owner" indicates that the relation is managed by pet.setOwner() method and not owner.getPets().add() method.
The fetch attribute is used to define the strategy for fetching the pet records from the database. With a FetchType.LAZY the set is populated only when accessed. However if we set it to FetchType.EAGER then the below code:
public static void fetchOwner() {
    EntityManager entityManager = emFactory.createEntityManager(); 
    System.out.println(entityManager.find(Owner.class, 1L));
}
will result in :
select
        owner0_.id as id0_1_,
        owner0_.NAME as NAME0_1_,
        owner0_.USER_ID as USER3_0_1_,
        pets1_.OWNER_ID as OWNER5_3_,
        pets1_.id as id3_,
        pets1_.id as id1_0_,
        pets1_.AGE as AGE1_0_,
        pets1_.NAME as NAME1_0_,
        pets1_.OWNER_ID as OWNER5_1_0_,
        pets1_.TAG_ID as TAG4_1_0_ 
    from
        firstOne.OWNER owner0_ 
    left outer join
        firstOne.PET pets1_ 
            on owner0_.id=pets1_.OWNER_ID 
    where
        owner0_.id = ?
As seen the pets were fetched along with the Owner record.
The last attribute is the targetEntity. It indicates the class type of the objects in the set. It can be avoided if we are using generics.
At the other side of the Pet-Owner association we have the Pet class:
@ManyToOne(fetch = FetchType.LAZY, optional = false, targetEntity = Owner.class)
@JoinColumn(name="OWNER_ID")
public Owner getOwner() {
    return owner;
}
Here many pets have one Owner. The @ManyToOne also includes a fetch attribute, cascadeType attribute and a targetEntity attribute. There is an additional optional attribute. If set to false it means that the owner cannot have a null value in the Pet object. I set the attribute to false and tried to create a Pet record:
public static void testCreatePet() {
    EntityManager entityManager = emFactory.createEntityManager(); 
    EntityTransaction transaction = entityManager.getTransaction();
    transaction.begin();
    Pet pet = new Pet();
    pet.setAge(3);
    pet.setName("timtom");
    pet.setTagId("2321");
    entityManager.persist(pet);
    transaction.commit();
}
The code threw the below exception:
Exception in thread "main" javax.persistence.PersistenceException: org.hibernate
.PropertyValueException: not-null property references a null or transient value:
 com.basic.Pet.owner
    at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(Abstra
ctEntityManagerImpl.java:614)
    at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImp
l.java:226)
    at com.test.Client.testCreatePet(Client.java:58)
    at com.test.Client.main(Client.java:85)
Caused by: org.hibernate.PropertyValueException: not-null property references a 
null or transient value: com.basic.Pet.owner
    at org.hibernate.engine.Nullability.checkNullability(Nullability.java:95)
    at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(Abs
tractSaveEventListener.java:313)
On the other hand if I execute the same code with optional = true the logs indicate a different failure:
Exception in thread "main" javax.persistence.PersistenceException: org.hibernate
.exception.ConstraintViolationException: could not insert: [com.basic.Pet]
    at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(Abstra
ctEntityManagerImpl.java:614)
    at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImp
l.java:226)
    at com.test.Client.testCreatePet(Client.java:58)
    at com.test.Client.main(Client.java:85)
Caused by: org.hibernate.exception.ConstraintViolationException: could not inser
t: [com.basic.Pet]2875 [main] DEBUG org.hibernate.util.JDBCExceptionReporter  - 
could not insert: [com.basic.Pet] [insert into firstOne.PET (AGE, NAME, OWNER_ID
, TAG_ID) values (?, ?, ?, ?)]
org.postgresql.util.PSQLException: ERROR: null value in column "owner_id" violat
es not-null constraint
  Detail: Failing row contains (1, 3, timtom, 2321, null).
As seen, when the optional attribute is set to false, before inserting the record Hibernate checks if the association is null and stops execution accordingly. However if the value is false, then the insert proceeds to database, where depending on constraints, the record may or may not be created. Setting the optional attribute to false for a non-null relation can thus save on an unwanted database trip in your code. Similarly optional also impacts the DDL generated:
with optional = true:
create table firstOne.PET (
        id  bigserial not null,
        AGE int4,
        NAME varchar(12) not null,
        TAG_ID varchar(255),
        OWNER_ID int8,
        primary key (id),
        unique (TAG_ID)
    )
and with optional = false:
create table firstOne.PET (
        id  bigserial not null,
        AGE int4,
        NAME varchar(12) not null,
        TAG_ID varchar(255),
        OWNER_ID int8 not null,
        primary key (id),
        unique (TAG_ID)
    )
Optional=false ensures that a not null constraint is added at the column level. Thus optional attribute plays an important role in both DDL creation and normal code execution. Along with the ManyToOne annotation we also included a JoinColumn annotation. This is an optional annotation. It indicates that the association maps to a foreign key column. The need for this annotation is that the ManyToOne annotation does not provide for any technique to specify the name of the join column. I removed the annotation and the column generated in DDL was named as "owner_id".

No comments:

Post a Comment