A reader commented on a different NHibernate post, asking about how one might do some different mappings. This post describes one of those scenarios: One-to-One mappings with a shared key. In this case, the parent entity has a surrogate key. The child is using the same value as its primary key and the same field as the foreign key to the parent.
Code samples for this article are on Github.
The Database
The test project is setup to run against a SQL Server database rather than some in-memory or mocked instance. I've created a quick SQL script to setup the datbase tables. SQL Server 2008 R2 was used for this example. The Express edition should work equally as well. We will be working with two tables:
The UserDetail.UserId is the same value as User.Id. Both are primary keys. There is a one-to-one relationship between the Users table and the UserDetail table. The UserDetail table, you will note, does not have an independent field for the foreign key.
The User Object
The first entity is the User. The User is the parent object. It is defined as follows. The AddDetail method is used to associate the different entities with each other. For example, if we have a new User and a new UserDetail, we can pass the UserDetail into the User.AddDetail() method. This will ensure that the UserDetail is correctly setup as the child of the User.
public class User { public virtual long Id { get; set; } public virtual string Name { get; set; } public virtual UserDetail Detail { get; set; } public virtual void AddDetail(UserDetail userDetail) { Detail = userDetail; Detail.AddUser(this); } }
The User object's mapping is pretty basic. It sets the identity column as being generated by the database. It also sets the one-to-one mapping for the detail.
public class UserMap : ClassMapping<User> { public UserMap() { Table("Users"); Id(user => user.Id, mapper => mapper.Generator(Generators.Identity)); Property(user => user.Name); OneToOne(user => user.Detail, mapper => { mapper.ForeignKey("FK_User_Id"); mapper.Cascade(Cascade.All); }); } }The UserDetail Object
Next up is the UserDetail. UserDetail is a child of User, having a one-to-one relationship with its parent. The UserDetail.AddUser() method performs a similar function to the User.AddDetail() method: it helps setup the association between the UserDetail object and a parent User object. The Equals() and GetHashCode() overrides are necessary to accommodate the way we'll be setting up the mapping.
public class UserDetail { public virtual long UserId { get; set; } public virtual string DriversLicense { get; set; } public virtual bool IsDonor { get; set; } public virtual User ParentUser { get; set; } public virtual void AddUser(User user) { ParentUser = user; UserId = user.Id; } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; var a = obj as UserDetail; if (a == null) return false; return a.UserId == UserId; } public override int GetHashCode() { unchecked { var hash = 21; hash = hash*37 + UserId.GetHashCode(); return hash; } } }
The UserDetail mapping is a little more involved. Identifier is setup as a composite ID. This allows us to use NHibernates mapper to select the column name and the foreign key value. It also imposes the requirement that we have created the Equals() and GetHashCode() overrides.
public class UserDetailMap : ClassMapping<UserDetail> { public UserDetailMap() { Table("UserDetail"); ComposedId(mapper => mapper.ManyToOne(o => o.ParentUser, x => { x.Column("UserId"); x.ForeignKey("FK_User_Id"); })); Property(detail => detail.DriversLicense); Property(detail => detail.IsDonor); } }
Persisting The Entities
One common mistake made when using NHibernate to persist related entities is to not set the relevant properties. In this case, the UserDetail.ParentUser property must be set, so that NHibernate can get the value of the ID property. I then to use a setter method to ensure any values are correctly set. In this case, it's the UserDetail.AddUser() method.
The tests file in the example solution gives two examples for persisting the entities to the database. It highlights how to ensure the critical step which happens just after creating the detailToBeSaved object.
[TestMethod] public void SavingAUserWithADetailObject() { var userToBeSaved = Builder<User>.CreateNew() .With(user1 => user1.Id = 0) .Build(); var detailToBeSaved = Builder<UserDetail>.CreateNew().Build(); // Don't forget the next line... userToBeSaved.AddDetail(detailToBeSaved); object save; using (var txn = session.BeginTransaction()) { save = session.Save(userToBeSaved); txn.Commit(); } var loadedUser = session.Get<User>(save); loadedUser.ShouldHave().AllPropertiesBut(user => user.Id).EqualTo(userToBeSaved); loadedUser.Detail.ShouldHave().AllProperties().EqualTo(detailToBeSaved); using (var txn = session.BeginTransaction()) { session.Delete(loadedUser); txn.Commit(); } }
A Common Problem
It's pretty common to forget to initialize the child object before persisting. In this case, the UserDetail cannot exist without its parent User. However, if we do not setup the properties on the UserDetail via the AddUser() method or some other means, NHibernate won't know how to associate things. This results in an exception like the following:
Test method Prabu.Tests.OneToOneTests.SavingAUserWithADetailObject threw exception: NHibernate.Exceptions.GenericADOException: could not execute batch command.[SQL: SQL not available] ---> System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'UserId', table 'Prabu.dbo.UserDetail'; column does not allow nulls. INSERT fails. The statement has been terminated.This happens because NHibernate has no way to figure out what it should be inserting in the UserDetail.UserId field. You can prove this by commenting out the lines of the AddUser() method, and running the test.
Summary
It really is easier and better to just have a surrogate key with which to work. NHibernate is flexible enough to accommodate times when the world isn't perfect. I hope this helped show how to get around one of those sticky situations.
Great post. Is there a reason not to add a Id for UserDetail table. Shouldn't it add a line of x.Unique(true) to ComposedId to make the row unique.
ReplyDeleteUserDetail represented an existing table which could not be altered for reasons beyond the developer's control.
DeleteI hadn't tested it, but it probably could have had the x.Unique(true) to the ComposedId. Since we were dealing with a 1:1 ratio anyway, the parent table's Id was enforcing the unique constraint anyway.