These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 10 posts ] 
Author Message
 Post subject: Bidirectional relationship without bidirectional schema?
PostPosted: Tue Nov 15, 2005 6:07 am 
Newbie

Joined: Tue Nov 15, 2005 5:41 am
Posts: 16
Need help with Hibernate? Read this first:
http://www.hibernate.org/ForumMailingli ... AskForHelp

Hi,
I have a problem with a bidirectional many-to-one relationship between address and contact entities as mapped below. Basically a contact
is mapped one-to-one with address. The contact table DDL looks like this:

# Host: localhost
# Database: lettings
# Table: 'contact'
#
CREATE TABLE `contact` (
`id` int(11) NOT NULL auto_increment,
`title_id_fk` int(11) NOT NULL default '0',
`first_name` varchar(50) NOT NULL default '',
`second_name` varchar(50) NOT NULL default '',
`middle_name_1` varchar(50) default NULL,
`middle_name_2` varchar(50) default NULL,
PRIMARY KEY (`id`),
KEY `title_ind` (`title_id_fk`),
CONSTRAINT `contact_ibfk_1` FOREIGN KEY (`title_id_fk`) REFERENCES `type` (`id`)
) TYPE=InnoDB;

Notice that the contact table has NO address foreign key.

The address table is mapped many-to-one with contact. The address table looks like this:

# Host: localhost
# Database: lettings
# Table: 'address'
#
CREATE TABLE `address` (
`id` int(11) NOT NULL auto_increment,
`line_1` varchar(100) NOT NULL default '',
`line_2` varchar(100) default NULL,
`line_3` varchar(100) default NULL,
`line_4` varchar(100) default NULL,
`line_5` varchar(100) default NULL,
`city` varchar(50) NOT NULL default '',
`postcode` varchar(10) NOT NULL default '',
`area_id_fk` int(11) NOT NULL default '0',
`contact_id_fk` int(11) NOT NULL default '0',
PRIMARY KEY (`id`),
UNIQUE KEY `contact_id_fk` (`contact_id_fk`),
KEY `area_ind` (`area_id_fk`),
KEY `contact_ind` (`contact_id_fk`),
CONSTRAINT `address_ibfk_1` FOREIGN KEY (`area_id_fk`) REFERENCES `area` (`id`),
CONSTRAINT `address_ibfk_2` FOREIGN KEY (`contact_id_fk`) REFERENCES `contact` (`id`)
) TYPE=InnoDB;

Notice that there IS a contact foreign key in the address table.

Now my problem is this. I currently create a Contact instance in memory and I then create the address instance in memory and set it as the Contact instances' address.

If I try and save the employer then I get an SQL exception complaining that the insert into address table cannot have a null contact_id_fk.

That is, there is nothing to tell hibernate that because an address has been set for a particular contact, that address's contact should be set to the contact (ie. make the association bidirectional).

Is there anyway to give hibernate a hint about this? Currently, just before my save of the Contact, I make this call:

contact.getAddress().setContact(contact);

which does the job.

But it seems a little unecessary.

Is there a way to make relationships bidirectional in hibernate, even though this bidirectionality is not strictly supported by the schema?

thanks a lot for any help - all the mappings etc. are below.

Neil

Hibernate version:2.1.7c

Mapping documents:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>

<class name="org.thorne.lettings.util.Type" table="type">
<id name="id" column="id" unsaved-value="-1">
<generator class="identity"/>
</id>
<discriminator column="type" type="string"/>
<property name="description" column="description"/>
<subclass name="org.thorne.lettings.referencedata.api.MaritalStatusImpl" discriminator-value="maritalStatus"/>
<subclass name="org.thorne.lettings.referencedata.api.EmploymentTypeImpl" discriminator-value="employment"/>
<subclass name="org.thorne.lettings.referencedata.api.TitleImpl" discriminator-value="title"/>
</class>

<class name="org.thorne.lettings.referencedata.api.AreaImpl" table="area">
<id name="id" column="id" unsaved-value="-1">
<generator class="identity"/>
</id>
<property name="name" column="name"/>
<property name="population" column="population"/>
<set name="addresses" cascade="all" inverse="true" >
<key column="area_id_fk"/>
<one-to-many class="org.thorne.lettings.referencedata.api.AddressImpl"/>
</set>
</class>

<class name="org.thorne.lettings.referencedata.api.TelephoneNumberImpl" table="telephone_number">
<id name="id" column="id" unsaved-value="-1">
<generator class="identity"/>
</id>
<property name="areaCode" column="area_code"/>
<property name="number" column="number"/>
<many-to-one name="contact" class="org.thorne.lettings.referencedata.api.Contact" column="contact_id_fk" cascade="all" />
</class>

<class name="org.thorne.lettings.referencedata.api.AddressImpl" table="address">
<id name="id" column="id" unsaved-value="-1">
<generator class="identity"/>
</id>
<property name="line1" column="line_1"/>
<property name="line2" column="line_2"/>
<property name="line3" column="line_3"/>
<property name="line4" column="line_4"/>
<property name="line5" column="line_5"/>
<property name="city" column="city"/>
<property name="postCode" column="postcode"/>
<many-to-one name="contact" class="org.thorne.lettings.referencedata.api.Contact" column="contact_id_fk" cascade="none"/>
<many-to-one name="area" class="org.thorne.lettings.referencedata.api.AreaImpl" column="area_id_fk" cascade="none" />
</class>

<class name="org.thorne.lettings.referencedata.api.Contact" table="contact">
<id name="id" column="id" unsaved-value="-1">
<generator class="identity"/>
</id>
<component name="personName" class="org.thorne.lettings.referencedata.api.PersonNameImpl">
<property name="firstName" column="first_name"/>
<property name="secondName" column="second_name"/>
<property name="middleName1" column="middle_name_1"/>
<property name="middleName2" column="middle_name_2"/>
<many-to-one name="title" class="org.thorne.lettings.referencedata.api.TitleImpl" column="title_id_fk" cascade="none" />
</component>
<one-to-one name="address" class="org.thorne.lettings.referencedata.api.AddressImpl" property-ref="contact" cascade="all"/>
<one-to-one name="homeNumber" class="org.thorne.lettings.referencedata.api.TelephoneNumberImpl" cascade="all" />
<one-to-one name="workNumber" class="org.thorne.lettings.referencedata.api.TelephoneNumberImpl" cascade="all"/>
<one-to-one name="mobileNumber" class="org.thorne.lettings.referencedata.api.TelephoneNumberImpl" cascade="all"/>
<joined-subclass name="org.thorne.lettings.referencedata.api.TenantImpl" table="tenant">
<key column="id"/>
<many-to-one name="maritalStatus" class="org.thorne.lettings.referencedata.api.MaritalStatusImpl" column="marital_status_id_fk" cascade="none" />
<property name="pets" column="has_pets"/>
<property name="petDescription" column="pet_description"/>
<property name="smoker" column="is_smoker"/>
<property name="children" column="has_children"/>
<component name="employmentDetails" class="org.thorne.lettings.referencedata.api.EmploymentDetailsImpl" >
<many-to-one name="employmentType" class="org.thorne.lettings.referencedata.api.EmploymentTypeImpl" column="employment_type_id_fk" cascade="none" />
<property name="occupation"/>
<many-to-one name="employer" class="org.thorne.lettings.referencedata.api.EmployerImpl" column="employer_id_fk" cascade="none" />
</component>
</joined-subclass>
<joined-subclass name="org.thorne.lettings.referencedata.api.EmployerImpl" table="employer">
<key column="id"/>
<set name="employees" cascade="none" inverse="true" >
<key column="employer_id_fk"/>
<one-to-many class="org.thorne.lettings.referencedata.api.TenantImpl"/>
</set>
</joined-subclass>
</class>

</hibernate-mapping>




Code between sessionFactory.openSession() and session.close(): Managed by Spring Framework

Full stack trace of any exception that occurs:

Name and version of the database you are using:MySQL 4.0.18

The generated SQL (show_sql=true):

Debug level Hibernate log excerpt:


Top
 Profile  
 
 Post subject:
PostPosted: Tue Nov 15, 2005 8:08 am 
Regular
Regular

Joined: Sat Nov 05, 2005 5:33 am
Posts: 70
Location: Linz, Austria
This has nothing to do with Hibernate.

If you want your address to know your contact, tell it.


I'm normally doing it like this:
Code:
class Address {
    private Contact contact;
   
    public void setContact(Contact _contact) {
        if (_contact != contact) {
            if (contact != null) {
                contact.setAddress(null);
            }
            contact = _contact;
            if (contact != null) {
                contact.setAddress(this);
            }
        }
    }
}


class Contact {
    private Address address;
   
    public void setAddress(Address _address) {
        if (_address != address) {
            if (address != null) {
                address.setContact(null);
            }
            address = _address;
            if (address != null) {
                address.setContact(this);
            }
        }
    }
}


The important part about this is that the objects themselves ensure the integrity of the object graph.
That way you don't need any code like you have before save. The objects themselves care for that.


Actually, from a Hibernate point of view, you wouldn't even need the information about the address in the contact class, since this isn't persisted to the database anyway.

_________________
hth,
Heinz
Don't forget to rate if this helped


Top
 Profile  
 
 Post subject:
PostPosted: Tue Nov 15, 2005 8:27 am 
Newbie

Joined: Tue Nov 15, 2005 5:41 am
Posts: 16
Thanks Heinz,

I appreciate the idea that the responsibility for an object's graph maintenance should reside with that object where possible.
It is a better solution codewise. I also made me think about removing the mapping info for address in contact.

But isnt' it worth putting this mapping information in as I get cascaded deletes when I delete a contact?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Nov 15, 2005 8:37 am 
Regular
Regular

Joined: Sat Nov 05, 2005 5:33 am
Posts: 70
Location: Linz, Austria
Neil T wrote:
Thanks Heinz,

I appreciate the idea that the responsibility for an object's graph maintenance should reside with that object where possible.
It is a better solution codewise. I also made me think about removing the mapping info for address in contact.

But isnt' it worth putting this mapping information in as I get cascaded deletes when I delete a contact?


I didnt' mean to say that you should remove the mapping information. I just wanted to point out that it isn't necessary for insert and update. Actually I forgot about cascading the delete.

I'd surely keep the mapping.


Regards,
Heinz


Top
 Profile  
 
 Post subject:
PostPosted: Tue Nov 15, 2005 11:56 am 
Newbie

Joined: Tue Nov 15, 2005 5:41 am
Posts: 16
Actually Heinz there is a big problem with this code you posted.

I tried to use it and you get StackOverflows. Basically you can get into a situation where you keep trying to set the current reference's association to null, but you never actually update the current reference to the new reference.

Essentially you never get past the --> line, in each class.

class Address {

public void setContact(Contact contact) {
if (this.contact != contact) {
if (this.contact != null) {
-->> this.contact.setAddress(null);
}
this.contact = contact;
if (this.contact != null) {
this.contact.setAddress(this);
}
}
}

}

class Contact{

public void setAddress(Address address) {
if (this.address != address) {
if (this.address != null) {
-->> this.address.setContact(null);
}
this.address = address;
if (this.address != null) {
this.address.setContact(this);
}
}
}

}

The only solution I can come up with is to have a separate releaseAddress method on Contact which is called when you want the contact association set to null, which should only really be called by the Address class.

That as opposed to a different caller actually wanting to set the Address to null.

So you have:

class Contact{

public void setAddress(Address address) {
if (this.address != address) {
if (this.address != null) {
this.address.releaseContact();
}
this.address = address;
if (this.address != null) {
this.address.setContact(this);
}
}
}

public void releaseAddress(){
this.address = null;
}

}

class Address {

public void setContact(Contact contact) {
if (this.contact != contact) {
if (this.contact != null) {
this.contact.releaseAddress();
}
this.contact = contact;
if (this.contact != null) {
this.contact.setAddress(this);
}
}
}

public void releaseContact(){
this.contact = null;
}

}


Top
 Profile  
 
 Post subject:
PostPosted: Wed Nov 16, 2005 4:02 am 
Regular
Regular

Joined: Sat Nov 05, 2005 5:33 am
Posts: 70
Location: Linz, Austria
Actually, that way you're breaking the consistency of the object graph again.

The problem with my code is that I wrote it up from my mind.
Therefore, two lines need to be changed and one is missing:
Code:
public void setContact(Contact contact) {
if (this.contact != contact) {
Contact old = this.contact;
this.contact = contact;
if (old != null) {
old.setAddress(null);
}
if (this.contact != null) {
this.contact.setAddress(this);
}
}
}


And of course the respective change in the other class.
A release function doesn't hurt either. As long as it also sets the other end to null.


That way, the object graph should stay consistent.

_________________
hth,
Heinz
Don't forget to rate if this helped


Top
 Profile  
 
 Post subject:
PostPosted: Wed Nov 16, 2005 5:25 am 
Newbie

Joined: Tue Nov 15, 2005 5:41 am
Posts: 16
Heinz - I'm sorry to be all picky but this doesn't work either.

I got this test case together

import junit.framework.TestCase;

/**
* @author Neil Thorne
*/
public class ContactAssociationTest extends TestCase {

public void testStackOverflow4() {
final Address address1 = new AddressImpl();
final Address address2 = new AddressImpl();
final Contact contact = new EmployerImpl();

contact.setAddress(address1);
contact.setAddress(address2);

assertTrue(address1.getContact() == null);
assertTrue(contact == address2.getContact());
assertTrue(address2.getContact() == contact);
}


public void testStackOverflow3() {
final Contact contact1 = new EmployerImpl();
final Contact contact2 = new EmployerImpl();
final Address address = new AddressImpl();

address.setContact(contact1);
address.setContact(contact2);
assertTrue(contact1.getAddress() == null);
assertTrue(contact2 == address.getContact());
assertTrue(contact2.getAddress() == address);
}


}


And it fails for your new implementation - mainly because you haven't copied the value for this.x into x you've just changed the reference - so when you change this.x to y, x is now pointing at y.

It's a trickier problem than I first thought. I've been given a couple of suggestions. One is to put a private boolean member settingContact into the Address class.

private boolean settingAddress = false;

public void setAddress(Address address) {
if (this.address != address && !settingAddress) {
settingAddress=true;
if (this.address != null) {
this.address.setContact(null)
}
this.address = address;
if (this.address != null) {
this.address.setContact(this);
}
settingAddress = false;
}
}

or you could go for a full blown observer implementation, whereby dependent objects actually register themselves as observers of each other. They temporarily remove themselves as an observer if they trigger are the trigger of a change event.


Top
 Profile  
 
 Post subject:
PostPosted: Wed Nov 16, 2005 6:04 am 
Regular
Regular

Joined: Sat Nov 05, 2005 5:33 am
Posts: 70
Location: Linz, Austria
The problem with the boolean variable is that it is not thread safe.


I did some tests with the code I posted.
Now I know where I went wrong (and why I had the set of the instance variable after the first set).

The second try is nearly OK.
The only problem is, you have to set this.address (or contact) to null before you reset the old. Then after you reset the old object, you set this.address.
Code:
   public void setAddress(Address address) {
      if (this.address != address) {
         Address old = this.address;
         this.address = null;
         if (old != null) {
            old.setContact(null);
         }
         this.address = address;
         if (this.address != null) {
            this.address.setContact(this);
         }
      }
   }


This time it's tested, too.

_________________
hth,
Heinz
Don't forget to rate if this helped


Top
 Profile  
 
 Post subject:
PostPosted: Wed Nov 16, 2005 5:48 pm 
Newbie

Joined: Tue Nov 15, 2005 5:41 am
Posts: 16
Thanks Heinz.

After all this discussion, I've found that if I try and manage these relationships myself in the object model I start to get all sorts of exceptions like unable to access LazyCollection, and deleted values being saved by cascades etc...

If I simply have standard setters and no more, then the exceptions go away.

eg.

class Contact {

private Address address;

public void setAddress{
this.address = address;
}

}

It's completely bizzare. If I disregard Hibernate and get a pure domain model that takes care of bidirectional relationships then all my domain tests pass.

But try and use Hibernate and it doesn't seem to like it.

Have you actually made object graphs that can manage themselves AND be persisted by Hibernate without confusion?


Top
 Profile  
 
 Post subject:
PostPosted: Wed Nov 16, 2005 6:30 pm 
Regular
Regular

Joined: Sat Nov 05, 2005 5:33 am
Posts: 70
Location: Linz, Austria
Neil T wrote:
Have you actually made object graphs that can manage themselves AND be persisted by Hibernate without confusion?


Acutally not, since I'm also just starting with Hibernate.

Would be interesting, what some of the more experienced people around have to say to this topic.

So long,
Heinz


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 10 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.