Is the following a bug, or is it possible to work around the problem I am having?
I have an organisation which can have a number of addresses. Other "things" in the database can also have addresses so I have an address table and use a linking table to model organisations having addresses. The linking table contains an organisation id and addresse id and and id which is the primary key. The organisation id and address id pair are obviously unique but I introduced the id field as hibernate doesn't seem to model composite keys in a particularly convenient manner.
A cut down version of the classes and their corresponding mapping files follow.
Organisation.cs:
Code:
[Serializable]
public class Organisation
{
private Int64 id;
private IList organisationAddressList;
private string name;
public Organisation()
{
id = 0;
organisationAddressList = new ArrayList();
name = String.Empty;
}
public virtual Int64 Id
{
get { return id; }
set
{
isChanged |= ( id != value );
id = value;
}
}
public virtual IList OrganisationAddressList
{
get
{
return organisationAddressList;
}
set
{
organisationAddressList = value;
}
}
public virtual string Name
{
get { return name; }
set
{
if( value == null )
throw new ArgumentOutOfRangeException("Null value not allowed for Name", value, "null");
if( value.Length > 50)
throw new ArgumentOutOfRangeException("Invalid value for Name", value, value.ToString());
isChanged |= (name != value); name = value;
}
}
}
Orgsanisiation.hbm.xml:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Services.BusinessEntities.Organisation,Services.BusinessEntities" table="Organisation">
<id name="Id" column="id" type="Int64" unsaved-value="0">
<generator class="identity"/>
</id>
<bag name="OrganisationAddressList" inverse="true" lazy="true" cascade="all-delete-orphan">
<key column="organisationId" />
<one-to-many class="Services.BusinessEntities.OrganisationAddress,Services.BusinessEntities" />
</bag>
<property column="name" type="String" name="Name" not-null="true" length="50" />
</hibernate-mapping>
OrganisationAddress.cs:
Code:
[Serializable]
public class OrganisationAddress
{
private Int64 id;
private Organisation organisation;
private Address address;
public OrganisationAddress()
{
id = 0;
organisation = new Organisation();
address = new Address();
}
public OrganisationAddress(
Organisation organisation,
Address address)
: this()
{
this.organisation = organisation;
this.address = address;
}
public virtual Int64 Id
{
get { return id; }
set
{
isChanged |= ( id != value );
id = value;
}
}
public virtual Organisation Organisation
{
get { return organisation; }
set
{
if( value == null )
throw new ArgumentOutOfRangeException("Null value not allowed for Organisation", value, "null");
isChanged |= ( organisation != value );
organisation = value;
}
}
public virtual Address Address
{
get { return address; }
set
{
if( value == null )
throw new ArgumentOutOfRangeException("Null value not allowed for Address", value, "null");
isChanged |= ( address != value );
address = value;
}
}
}
OrganisationAddress.hbm.xml:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Services.BusinessEntities.OrganisationAddress,Services.BusinessEntities" table="OrganisationAddress">
<id name="Id" column="id" type="Int64" unsaved-value="0">
<generator class="identity"/>
</id>
<many-to-one name="Organisation" column="organisationId" class="Services.BusinessEntities.Organisation,Services.BusinessEntities" />
<many-to-one name="Address" column="addressId" class="Services.BusinessEntities.Address,Services.BusinessEntities" cascade="all-delete-orphan"/>
</class>
</hibernate-mapping>
Address.cs:
Code:
[Serializable]
public class Address
{
private Int64 id;
private IList organisationAddressList;
private string postcode;
public Address()
{
id = 0;
organisationAddressList = new ArrayList();
postcode = String.Empty;
}
public virtual Int64 Id
{
get { return id; }
set
{
isChanged |= ( id != value );
id = value;
}
}
public virtual IList OrganisationAddressList
{
get
{
return organisationAddressList;
}
set
{
organisationAddressList = value;
}
}
public virtual string Postcode
{
get { return postcode; }
set
{
if( value != null && value.Length > 50)
throw new ArgumentOutOfRangeException("Invalid value for Postcode", value, value.ToString());
isChanged |= (postcode != value); postcode = value;
}
}
}
Address.hbm.xml:
Code:
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
<class name="Services.BusinessEntities.Address,Services.BusinessEntities" table="Address">
<id name="Id" column="id" type="Int64" unsaved-value="0">
<generator class="identity"/>
</id>
<bag name="OrganisationAddressList" inverse="true" lazy="true" >
<key column="addressId" />
<one-to-many class="Services.BusinessEntities.OrganisationAddress,Services.BusinessEntities" />
</bag>
<property column="postcode" type="String" name="Postcode" length="50" />
</class>
</hibernate-mapping>
Between my data layer and the front end (amongst other things) I have a translation layer which converts the "hibernate" classes into "front-end" classes. Since the id of the OrgnisationAddress table is just an arbitrary id introduced for my convenience I don't wish to pass this upto the front-end.
When I receive the data back from the front-end in order to do the update, I use the organisation id and address id, to read the entities from the database and then update these with the data from the front-end. Since I no longer know what the ID of the OrgnisationAddress is, I simply create a new instance of it. See below:
Code:
public static Organisation Convert(OrganisationDataType from)
{
// service is a business logic object which eventually delegates
// to an NHibernate session
Organisation to = service.GetOrganisation(from.Id);
to.Name = from.Name;
Convert(to, to.OrganisationAddressList, from.Addresses);
return to;
}
private static void Convert(Organisation parent, IList to, IList<AddressDataType> from)
{
if (from == null)
{
throw new ServImpTranslationException("Orgnisation address list must not be null");
}
to.Clear();
foreach (AddressDataType address in from)
{
to.Add(new OrganisationAddress(parent, Convert(address)));
}
}
private static Address Convert(AddressDataType from)
{
Address to = service.GetAddress(from.Id);
if (to == null)
{
to = new Address();
}
to.Postcode = from.Postcode;
return to;
}
When I then try and pass the updated Orgnisation object to SaveOrUpdateCopy I get the following exception:
Code:
NHibernate.ADOException : could not delete: [Services.BusinessEntities.Address#2815][SQL: DELETE FROM abc.Address WHERE id = ?]
----> System.Data.SqlClient.SqlException : The DELETE statement conflicted with the REFERENCE constraint "FK_OrganisationAddress_Address". The conflict occurred in database "abc", table "abc.OrganisationAddress", column 'addressId'.
The statement has been terminated.
Looking at the output from NHibernate it is easy to see what has happened:
Code:
NHibernate: INSERT INTO abc.OrganisationAddress (organisationId, addressId, isMainAddress) VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY(); @p0 = '3811', @p1 = '2815', @p2 = 'True'
NHibernate: DELETE FROM abc.OrganisationAddress WHERE id = @p0; @p0 = '946'
NHibernate: DELETE FROM abc.Address WHERE id = @p0; @p0 = '2815'
Hibernate is correctly inserting a new entry into the OrgnisationAddress table for organisation 3811 and address 2815, and it is correctly deleting the old entry. However, the delete continues to cascade down to the Address and because address 2815 is referenced in the OrganisationAddress table, the error occurs - I think that NHibernate should recognise that Address is NOT an orphan and therefore not try and delete Address 2815.
I use this pattern to deal with lookup tables like this one elsewhere in the system and it works well. However, the big difference is that I have not got cascade set to all-delete-orphan - I need it here however.
One thing that I considered was that the problem was caused because Address.OrgnisationAddressList did not contain an entry for the newly created OrganisationAddress, so after I created it I tried adding it to the list myself - this had no affect whatsoever.
I am using version 1.2.0 of NHibernate.
Thanks
Simon