In this chapter, we will be covering another feature which is Inverse Relationships. It is an amusing option that you will see on collection that are inversely equal to true and it also confuses a lot of developers. So let's talk about this option. To understand this, you really have to think about the relational model. Let’s say you have a bidirectional associations using a single foreign key.
From a relational standpoint, you have got one foreign key, and it represents both customer to order and orders to customer.
From the OO model, you have unidirectional associations using these references.
There is nothing that says that two unidirectional associations represent the same bidirectional association in the database.
The problem here is that NHibernate doesn't have enough information to know that customer.orders and order.customer represent the same relationship in the database.
We need to provide inverse equals true as a hint, it is because the unidirectional associations are using the same data.
If we try to save these relationships that have 2 references to them, NHibernate will try to update that reference twice.
It will actually do an extra roundtrip to the database, and it will also have 2 updates to that foreign key.
The inverse equals true tells NHibernate which side of the relationship to ignore.
When you apply it to the collection side and NHibernate will always update the foreign key from the other side, from the child object side.
Then we only have one update to that foreign key and we don't have additional updates to that data.
This allows us to prevent these duplicate updates to the foreign key and it also helps us to prevent foreign key violations.
Let's have a look at the customer.cs file in which you will see the AddOrder method and the idea here is that we now have this back pointer from order back to customer and it needs to be set. So when an order is added to a customer, that customer's back pointer is set, otherwise, it would be null, so we need this to keep this connected properly together in the object graph.
using System; using System.Text; using Iesi.Collections.Generic; namespace NHibernateDemo { public class Customer { public Customer() { MemberSince = DateTime.UtcNow; Orders = new HashedSet<Order>(); } public virtual Guid Id { get; set; } public virtual string FirstName { get; set; } public virtual string LastName { get; set; } public virtual double AverageRating { get; set; } public virtual int Points { get; set; } public virtual bool HasGoldStatus { get; set; } public virtual DateTime MemberSince { get; set; } public virtual CustomerCreditRating CreditRating { get; set; } public virtual Location Address { get; set; } public virtual ISet<Order> Orders { get; set; } public virtual void AddOrder(Order order) { Orders.Add(order); order.Customer = this; } public override string ToString() { var result = new StringBuilder(); result.AppendFormat("{1} {2} ({0})\r\n\tPoints: {3}\r\n\tHasGoldStatus: {4}\r\n\tMemberSince: {5} ({7})\r\n\tCreditRating: {6}\r\n\tAverageRating: {8}\r\n", Id, FirstName, LastName, Points, HasGoldStatus, MemberSince, CreditRating, MemberSince.Kind, AverageRating); result.AppendLine("\tOrders:"); foreach(var order in Orders) { result.AppendLine("\t\t" + order); } return result.ToString(); } } public class Location { public virtual string Street { get; set; } public virtual string City { get; set; } public virtual string Province { get; set; } public virtual string Country { get; set; } } public enum CustomerCreditRating { Excellent, VeryVeryGood, VeryGood, Good, Neutral, Poor, Terrible } }
Here is the Program.cs file implementation.
using System; using System.Data; using System.Linq; using System.Reflection; using HibernatingRhinos.Profiler.Appender.NHibernate; using NHibernate.Cfg; using NHibernate.Dialect; using NHibernate.Driver; using NHibernate.Linq; namespace NHibernateDemo { internal class Program { private static void Main() { var cfg = ConfigureNHibernate(); var sessionFactory = cfg.BuildSessionFactory(); Guid id; using(var session = sessionFactory.OpenSession()) using(var tx = session.BeginTransaction()) { var newCustomer = CreateCustomer(); Console.WriteLine("New Customer:"); Console.WriteLine(newCustomer); session.Save(newCustomer); id = newCustomer.Id; tx.Commit(); } using(var session = sessionFactory.OpenSession()) using(var tx = session.BeginTransaction()) { var query = from customer in session.Query<Customer>() where customer.Id == id select customer; var reloaded = query.Fetch(x => x.Orders).ToList().First(); Console.WriteLine("Reloaded:"); Console.WriteLine(reloaded); tx.Commit(); } Console.WriteLine("Press <ENTER> to exit..."); Console.ReadLine(); } private static Customer CreateCustomer() { var customer = new Customer { FirstName = "John", LastName = "Doe", Points = 100, HasGoldStatus = true, MemberSince = new DateTime(2012, 1, 1), CreditRating = CustomerCreditRating.Good, AverageRating = 42.42424242, Address = CreateLocation() }; var order1 = new Order { Ordered = DateTime.Now }; customer.AddOrder(order1); var order2 = new Order { Ordered = DateTime.Now.AddDays(-1), Shipped = DateTime.Now, ShipTo = CreateLocation() }; customer.AddOrder(order2); return customer; } private static Location CreateLocation() { return new Location { Street = "123 Somewhere Avenue", City = "Nowhere", Province = "Alberta", Country = "Canada" }; } private static Configuration ConfigureNHibernate() { NHibernateProfiler.Initialize(); var cfg = new Configuration(); cfg.DataBaseIntegration(x => { x.ConnectionStringName = "default"; x.Driver<SqlClientDriver>(); x.Dialect<MsSql2008Dialect>(); x.IsolationLevel = IsolationLevel.RepeatableRead; x.Timeout = 10; x.BatchSize = 10; }); cfg.SessionFactory().GenerateStatistics(); cfg.AddAssembly(Assembly.GetExecutingAssembly()); return cfg; } } }
It is going to save that to the database and then reload it. Now let’s run your application and open the NHibernate Profiler and see how it actually saved it.
You will notice that we have 3 groups of statements. The first one will insert the customer, and that customer's ID is the Guid, which is highlighted. The second statement is insert into the orders table.
You will notice the same Customer Id Guid is set in there, so have that foreign key set. The last statement is the update, which will update the foreign key to the same customer id once again.
Now the problem is that the customer has the orders, and the orders has the customer, there's no way that we haven't told NHibernate that it's actually the same relationship. The way we do this is with inverse equals true.
So let's go to our customer.hbm.xml mapping file and set the inverse equal to true as shown in the following code.
<?xml version = "1.0" encoding = "utf-8" ?> <hibernate-mapping xmlns = "urn:nhibernate-mapping-2.2" assembly = "NHibernateDemo" namespace = "NHibernateDemo"> <class name = "Customer"> <id name = "Id"> <generator class = "guid.comb"/> </id> <property name = "FirstName"/> <property name = "LastName"/> <property name = "AverageRating"/> <property name = "Points"/> <property name = "HasGoldStatus"/> <property name = "MemberSince" type = "UtcDateTime"/> <property name = "CreditRating" type = "CustomerCreditRatingType"/> <component name = "Address"> <property name = "Street"/> <property name = "City"/> <property name = "Province"/> <property name = "Country"/> </component> <set name = "Orders" table = "`Order`" cascade = "all-delete-orphan" inverse = "true"> <key column = "CustomerId"/> <one-to-many class = "Order"/> </set> </class> </hibernate-mapping>
When saving the orders, it will set that foreign key from the order side. Now let’s run this application again and open the NHibernate profiler.
If we look at how those are inserted, we get the insert in the customer, and the insert into orders, but we don't have that duplicate update of the foreign key because it's being updated when the orders are being saved.
Now, you should note that if you only have a unidirectional association and it's the set that is maintaining this relationship, then if you turn inverse equals true, that foreign key is never going to be set, and those items are never going to have their foreign keys set in the database.
If you look at the many-to-one relationship in the Order.hbm.xml file and you look for inverse, it doesn't actually have an inverse attribute.
It always is set from the child item, but if you have a many-to-many collection, you can set it from either side.