Skip to content
Snippets Groups Projects
Commit 96a2db5a authored by Goik Martin's avatar Goik Martin
Browse files

Natural keys, equals and lazy fetchmode

parent 6e741b11
No related branches found
No related tags found
No related merge requests found
......@@ -17800,7 +17800,7 @@ CREATE TABLE BankAccount (
</glosslist>
 
<section xml:id="objectEqualityByPrimaryKey">
<title>Defining object equality</title>
<title>Defining object equality by primary key</title>
 
<para>Consider the following code:</para>
 
......@@ -17847,15 +17847,259 @@ second transaction: user.equals(user2):false <coref linkend="rereadInstance_2"/>
 
<para>The two instances in question definitely represent the same
database entity. The two entity managers referring to different
sessions create two disting instances within the java
sessions create two distinct instances within a given <link
linkend="gloss_Java"><acronym>Java</acronym></link>
runtime.</para>
 
<para>Since <link linkend="gloss_JPA"><abbrev>JPA</abbrev></link>
entities require an <code>@javax.persistence.Id</code> attribute
we may generally define object equality solely based on this
attribute's value:</para>
we may generally define object equality solely based on this at
tribute's value:</para>
 
<programlisting/>
<programlisting>package session2;
...
public class User {
@Id @GeneratedValue
private Long id;
...
@Override public boolean equals(Object other) {
if (this == other) <co linkends="equalByPrimaryKey-1"
xml:id="equalByPrimaryKey-1-co"/>{
return true;
} else if (id == null) {
return false;
} else if (other instanceof User) {
final User that = (User) other;
return this.id.equals(that.getId()) <co linkends="equalByPrimaryKey-2"
xml:id="equalByPrimaryKey-2-co"/>;
} else {
return false;
}
}
@Override public int hashCode() { <co linkends="equalByPrimaryKey-3"
xml:id="equalByPrimaryKey-3-co"/>
if (null == id) {
return System.identityHashCode(this);
} else {
return id.hashCode();
}
}
}</programlisting>
<para>This way of defining
<methodname>session2.User.equals(java.lang.Object)</methodname>
implies that either or both of the following two conditions must
be met:</para>
<calloutlist>
<callout arearefs="equalByPrimaryKey-1-co"
xml:id="equalByPrimaryKey-1">
<para>Both instances must be identical.</para>
</callout>
<callout arearefs="equalByPrimaryKey-2-co"
xml:id="equalByPrimaryKey-2">
<para>Both instances must have the same
<methodname>session2.User.getId()</methodname> value .</para>
</callout>
</calloutlist>
<caution>
<para>Do not forget to implement
<methodname>Object.hashCode()</methodname> <coref
linkend="equalByPrimaryKey-3-co" xml:id="equalByPrimaryKey-3"/>
accordingly: Two instances <code>a</code> and <code>b</code>
returning <code>a.equals(b) == true</code> equal must return an
identical hash code value <code>a.hashCode() ==
b.hashCode()</code> in order to satisfy a collection's
contract.</para>
</caution>
</section>
<section xml:id="objectEqualityByNaturalKey">
<title>Object equality by natural key</title>
<para>Defining entity equality based on database identity suffers
a severe deficiency: Newly created instances invariably differ
from any foreign non-identical instance regardless whether it does
have a database identity or not. We consider an example:</para>
<programlisting>package session2;
...
public class CompareNewlyCreated { ...
// Create two transient instances
final User a = new User(123, "goik", "Martin Goik"), <co
linkends="compareTransientById-1_2"
xml:id="compareTransientById-1-co"/>
b = new User(123, "goik", "Martin Goik"); <co
linkends="compareTransientById-1_2"
xml:id="compareTransientById-2-co"/>
System.out.println("a.equals(b):" + a.equals(b)); <co
linkends="compareTransientById-3"
xml:id="compareTransientById-3-co"/>
{
final Session session = HibernateUtil.createSessionFactory("session2/hibernate.cfg.xml").openSession();
final Transaction transaction = session.beginTransaction();
// previously saved as new User(123, "goik", "Martin Goik");
final User user = (User) session.load(User.class, 1L); <co
linkends="compareTransientById-4"
xml:id="compareTransientById-4-co"/>
System.out.println("a.equals(user)):" + a.equals(user)); <co
linkends="compareTransientById-5"
xml:id="compareTransientById-5-co"/>
transaction.commit();
session.close();
} ...</programlisting>
<calloutlist>
<callout arearefs="compareTransientById-1-co compareTransientById-2-co"
xml:id="compareTransientById-1_2">
<para><!--1 + 2-->Create two transient instances being
identical by value.</para>
</callout>
<callout arearefs="compareTransientById-3-co"
xml:id="compareTransientById-3">
<para><!--3-->Both instances are defined to differ by
value.</para>
</callout>
<callout arearefs="compareTransientById-4-co"
xml:id="compareTransientById-4">
<para><!--4-->Load a persistent entity from the
database.</para>
</callout>
<callout arearefs="compareTransientById-5-co"
xml:id="compareTransientById-5">
<para><!--5-->Transient and persistent instances are defined
to differ by value.</para>
</callout>
</calloutlist>
<para>Apparently this is definitely wrong: We do have unique
database index definitions. All objects in question do have common
values 123 and <code>"goik"</code> on these respective
keys.</para>
</section>
<section xml:id="equalityByNaturalKey">
<title>Implementing <methodname>Object.equals(Object)</methodname>
by natural keys</title>
<para>The last section's result actually provides a hint to
implement <methodname>Object.equals(Object)</methodname> in a more
meaningful way. The problem comparing transient instances occurs
since a surrogate key's value is being provided by the database
server when an entity is being persisted. If at least one natural
key (sometimes referred to as <quote>business key</quote>) is
being defined this one may be used instead:</para>
<figure xml:id="implementEqualsByNaturalKey">
<title>Implementing
<methodname>Object.equals(Object)</methodname> by natural
keys</title>
<programlisting>package session3;
@Entity
@Table(uniqueConstraints={@<emphasis role="bold">UniqueConstraint(columnNames={"uid"}</emphasis>)<co
linkends="implementEqualsByNaturalKey-1"
xml:id="implementEqualsByNaturalKey-1-co"/> ,
@UniqueConstraint(columnNames={"uidNumber"})})
public class User {
...
String uid;
@Column(nullable=false)
public String getUid() {return uid;}
public void setUid(String uid) {this.uid = uid;}
...
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (getUid() == null) {
return false;
} else if (other instanceof User) {
final User that = (User) other;
return <emphasis role="bold">this.getUid().equals( that.getUid() )</emphasis>; <co
linkends="implementEqualsByNaturalKey-2"
xml:id="implementEqualsByNaturalKey-2-co"/>
} else {
return false;
}
}
@Override
public int hashCode() {
if (null == getUid()) {
return System.identityHashCode(this);
} else {
return <emphasis role="bold">getUid().hashCode()</emphasis>; <co
linkends="implementEqualsByNaturalKey-3"
xml:id="implementEqualsByNaturalKey-3-co"/>
}
}
}</programlisting>
<calloutlist>
<callout arearefs="implementEqualsByNaturalKey-1-co"
xml:id="implementEqualsByNaturalKey-1">
<para>Definition of property
<methodname>session3.User.getUid()</methodname> to become a
natural key.</para>
</callout>
<callout arearefs="implementEqualsByNaturalKey-2-co"
xml:id="implementEqualsByNaturalKey-2">
<para>Two <classname>session3.User</classname> instances
having identical
<methodname>session3.User.getUid()</methodname> values will
be considered equal.</para>
</callout>
<callout arearefs="implementEqualsByNaturalKey-3-co"
xml:id="implementEqualsByNaturalKey-3">
<para>The <methodname>session3.User.hashCode()</methodname>
implementation has to be changed accordingly.</para>
</callout>
</calloutlist>
</figure>
<qandaset role="exercise">
<qandadiv>
<qandaentry>
<question>
<para>Consider <xref
linkend="implementEqualsByNaturalKey"/>. You may get a
different runtime behaviour when using <emphasis
role="bold"><code>this.uid().equals( that.uid()
)</code></emphasis> at <coref
linkend="implementEqualsByNaturalKey-2-co"/>. Execute the
corresponding <emphasis
role="bold">session3.CompareNewlyCreated</emphasis> and
<emphasis role="bold">session3.LoadUser</emphasis>
applications and explain the result.</para>
</question>
<answer>
<para><link linkend="gloss_JPA">JPA</link> allows lazy
fetch mode typically enabled by default. So the
<code>uid</code> attribute's initialization will be
deferred until
<methodname>session3.User.getUid()</methodname> is being
called for the first time.</para>
</answer>
</qandaentry>
</qandadiv>
</qandaset>
</section>
</section>
 
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment