It is important to note that the role-to-one relation defined in the Person mapping is declaredUniqueTo avoid that a single photo is assigned to more than one person. Also I define that each insert, update and delete action applied to a person entity shoshould be cascaded to the associated PersonPhoto entity.
Unit testing the XML mapping
Let's now write some unit tests that verify that my requirements are indeed satisfied.
[Updated]
First I want to analyze what database schema is created. remember: whenever I have the possibility to start a so called green field project I always start by defining the domain and then let the database auto-generated from the domain (that is: the database is only an implementation detail ). things might be different if you have to use a pre-existing database though...
So my unit test to verify that the schema can be created is
[TestFixture]
public class when_creating_the_schema : Person_Fixture
{
protected override void Context()
{
base.Context();
new SchemaExport(Configuration).Execute(true, false, false, false, Session.Connection, null);
}
[Test]
public void smoke_test()
{
true.ShouldBeTrue();
}
}
In the last line of the Context method I useSchemaExportClass of nhibto to generate the database schema from the model and the mapping information. the first (and only) test I write is a so called smoke test, that is it's a dummy test which shocould run just to test that setting up the context doesn't throw an exception. and indeed the test succeeds and the following output is produced
The script is generated forSQLiteDatabase I use. For a different type of database server the script wocould look slightly different. The two tables created are
create table Person (
Id UNIQUEIDENTIFIER not null,
LastName TEXT,
FirstName TEXT,
PersonPhotoId UNIQUEIDENTIFIER unique,
primary key (Id)
)
create table PersonPhoto (
Id UNIQUEIDENTIFIER not null,
Image BLOB,
primary key (Id)
)
Second I want to test whether I can create a new person object having a photo and save this aggregate to the database.
[TestFixture]
public class when_adding_a_new_person_with_a_photo : Person_Fixture
{
private PersonPhoto photo;
private Person person;
protected override void Context()
{
base.Context();
photo = new PersonPhoto {Image = Encoding.Default.GetBytes("This is a placeholder for a photo...")};
person = new Person("Schenker", "Gabriel", photo);
Session.Save(person);
// clean up
Session.Flush();
Session.Clear();
}
}
In the above code I have setup the context, that is-I want to add a new person with photo the database-(please have a look at the source code regarding the base class Person_Fixture which I use)
The first test I write is again smoke test, which shoshould run just to test that setting up the context doesn't throw an exception.
// Smoke test
[Test]
public void should_execute_without_an_error()
{
true.ShouldBeTrue();
}
And indeed, if you run this test it is green. This tells me that the method Context is running without causing an exception.
Remarks: AllShouldXXXMethods you'll see in the presented code fragments are extension methods I defined and use for better readability of the code. You can find them here.
The second test method checks whether there exists indeed a person record in the database
[Test]
public void person_should_exist_in_the_database()
{
var fromDb = Session.Get<Person>(person.Id);
fromDb.ShouldNotBeNull();
fromDb.ShouldNotBeTheSameAs(person);
fromDb.LastName.ShouldEqual(person.LastName);
fromDb.FirstName.ShouldEqual(person.FirstName);
}
In the above code I first check whether a non null object is loaded from db and then whether it is a different instance (that is it has not been just loaded from the first level cache of nhib.pdf --> please see also this post) which means it has really been loaded from the database. finally I check some of the properties for equality. as you might have expected: this test is green when run.
Now I want to also check whether the photo has really been stored in the database or not. The following test shocould confirm that
[Test]
public void person_photo_should_exist_in_the_database()
{
var fromDb = Session.Get<Person>(person.Id);
fromDb.Photo.ShouldNotBeNull();
fromDb.Photo.ShouldNotBeTheSameAs(person.Photo);
fromDb.Photo.Image.ShouldEqual(person.Photo.Image);
}
Well, green again when run-so no problem!
One important thing to check is that the very same photo cannot assigned to two different person instances. Each photo is uniquely assigned to a certain person. The following test verifies that behavior.
[Test]
public void adding_another_person_with_same_photo_should_not_be_possible()
{
var otherPerson = new Person("Doe", "John", photo);
Session.Save(otherPerson);
try
{
Session.Flush();
Assert.Fail("Expected exception!");
}
catch(HibernateException)
{
Session.Clear();
}
}
This code needs some further explanation. as you probably can see I have CT that NHibernate throws an exception when trying to add a new person having the same photo as an already existing person. the exception is not thrown when callingSaveMethod of the session but only when the session isFlushed(That is: the insert command is executed on the database ). I clear the session in the catch block since otherwise the exception wocould be raised again by my test tear-down method (in the base fixture class) which also flushes and disposes the session object.
The above test also passes and thus we are left with only one additional test. Does the person photo indeed lazy load? Let's write a test which verifies this
[TestFixture]
public class when_loading_an_existing_person_from_database : Person_Fixture
{
private PersonPhoto photo;
private Person person;
protected override void Context()
{
base.Context();
photo = new PersonPhoto { Image = Encoding.Default.GetBytes("This is a placeholder for a photo...") };
person = new Person("Schenker", "Gabriel", photo);
Session.Save(person);
// clean up
Session.Flush();
Session.Clear();
}
[Test]
public void Person_photo_should_be_lazy_loaded()
{
var fromDb = Session.Load<Person>(person.Id);
NHibernateUtil.IsInitialized(fromDb.Photo).ShouldBeFalse();
var image = fromDb.Photo.Image;
NHibernateUtil.IsInitialized(fromDb.Photo.Image).ShouldBeTrue();
}
}
Again I first setup the context for the test, that is I add a person with a photo to the database. then in the test method I load the previusly save person from database and with the aid of a utility class of NHibernate I check whether the propertyPhotoOf the person entity is un-initialized (I. e. the entity behind the property is not loaded ). then I access the property and finally I use the utility class again to test whether now the photo has been (lazy) loaded.
Wow-the test passes! We have found a mapping which satisfies all our needs!
Mapping with Fluent nhib.pdf
As described in earlier posts (part 1, part 2, part 3 and part 4) we have the possibility to useFluent nhib.pdfTo map our entities. Mapping this way has advantages (I already discussed in the posts just mentioned). Let's have a look at the mapping needed for the Person entity
public class PersonMapper : ClassMap<Person>
{
public PersonMapper()
{
LazyLoad();
Id(x => x.Id);
Map(x => x.FirstName);
Map(x => x.LastName);
References(x => x.Photo)
.FetchType.Select()
.Cascade.All()
.TheColumnNameIs("PersonPhotoId")
.WithUniqueConstraint();
}
}
I define a mapper class which inherits form the genericClassMap <T>Base class provided by the Fluent nhib1_framework. in the constructor of this class I define the mapping. the first line defines that I want my person entity to be lazy loaded (by default in Fluent NHibernate all entities are NOT lazy loaded ).
The role-to-one relation between person and person photo is mapped with the aid ofReferencesMethod.
The PersonPhoto entity is mapped as follows
public class PersonPhotoMapper : ClassMap<PersonPhoto>
{
public PersonPhotoMapper()
{
LazyLoad();
Id(x => x.Id);
Map(x => x.Image);
HasOne(x => x.Owner)
.PropertyRef(x=>x.Photo)
.Constrained();
}
}
Here the part that interests us most (the reverse relation from photo person) is mapped with the aid ofHasOneMethod.
Unit testing the Fluent nhib1_mapping
There is not much to say about this. the unit test are nearly the same with only one difference. I use a different base class from which I derive all my test classes. the definition of the base class used can be found here.
Code
You can find the accompanying this post here.
Summary
I have shown you a way how you can structure your domain model and map your entities to be able to lazy load "extra" information of a given entity. I have explained how to map the domain by using standard XML mapping files as well as by usingFluent nhib.pdfFramework. By applying this technique you can massively improve the performance of queries and reduce the bandwidth needed to transfer data from the database to the consuming client.
[Update] Uni-directional link between Person and PersonPhoto
In a comment I was asked why I implemented the relation between the person and the person photo entity as bi-directional and whether it wocould not be possible to only implement and uni-directional realtion between person and person photo.
The answer is: there isNOSpecial reason for having a bi-directional relation (Eric Evans in his DDD book even suggests to keep the relations uni-directional whenever possible ). and yes, it is possible to implement the sample with an uni-directional relation. i'll show the details below (this time I only show the mapping in Fluent NHibernate but the XML mapping is straight forward.
Here is the model with only an uni-directional relation
And the code
public class Person
{
public virtual Guid Id { get; private set; }
public virtual string LastName { get; private set; }
public virtual string FirstName { get; private set; }
public virtual PersonPhoto Photo { get; private set; }
// to satisfy NHibernate only!
public Person() { }
public Person(string lastName, string firstName, PersonPhoto personPhoto)
{
LastName = lastName;
FirstName = firstName;
AssignPhoto(personPhoto);
}
public virtual void AssignPhoto(PersonPhoto photo)
{
Photo = photo;
}
}
public class PersonPhoto
{
public virtual Guid Id { get; set; }
public virtual byte[] Image { get; set; }
}
Now let me show the mapping for this model (Fluent nhib.pdf)
public class PersonMapper : ClassMap<Person>
{
public PersonMapper()
{
LazyLoad();
Id(x => x.Id);
Map(x => x.FirstName);
Map(x => x.LastName);
References(x => x.Photo)
.FetchType.Select()
.Cascade.All()
.TheColumnNameIs("PersonPhotoId")
.WithUniqueConstraint();
}
}
public class PersonPhotoMapper : ClassMap<PersonPhoto>
{
public PersonPhotoMapper()
{
LazyLoad();
Id(x => x.Id);
Map(x => x.Image);
}
}
If I generate the schema from this model I get the very same create table scripts as in the bi-directional sample. And all the other unit tests I have shown above run successfully.
The code has been updated to contain both samples the uni-and the bi-directional relation.
Enjoy