From 96a2db5a486ad3de34afb088bd99002631cb072c Mon Sep 17 00:00:00 2001 From: Martin Goik <goik@hdm-stuttgart.de> Date: Mon, 15 Apr 2013 22:34:46 +0200 Subject: [PATCH] Natural keys, equals and lazy fetchmode --- Doc/course.xml | 254 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 249 insertions(+), 5 deletions(-) diff --git a/Doc/course.xml b/Doc/course.xml index a76f931c2..21e2c700d 100644 --- a/Doc/course.xml +++ b/Doc/course.xml @@ -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> -- GitLab