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