关于one-to-one的lazy load(NHibernate)

ref:

http://blogs.hibernatingrhinos.com/nhibernate/archive/2008/11/17/lazy-loading-blobs-and-the-like-in-nhibernate.aspx

 

========================================

 

One of the questions that is asked again and again in the NHibernate user mailing list is the question about whether NHibernate supports lazy-loading of properties. The answer is NO - at least for the time being. Why is this question reasonable? Well, often we have entities in our domain that contain fields with large amount of data. Some samples are

  • a large binary object (BLOB, e.g. an image, a Word document, a PDF, etc.),
  • a large text object (CLOB, or nvarchar(max) )
  • a cluster of rarely used extra fields

The problem is that we do not always need all this information when loading an entity. Thus we can massively improve the performance of our queries if those fields would only be loaded on demand.

The Model

Let's have a look at the following simplified domain model.

model

Here the person entity has an associated photo. The photo has been extracted from the person entity since NHibernate does not support lazy load of specific properties of an entity (as mentioned above) and thus each time we load a person entity we would also load its photo which might be huge (e.g. several MB).

The code for the person entity is simple

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!
    protected Person() { }
 
    public Person(string lastName, string firstName, PersonPhoto personPhoto)
    {
        LastName = lastName;
        FirstName = firstName;
        AssignPhoto(personPhoto);
    }
 
    public virtual void AssignPhoto(PersonPhoto photo)
    {
        Photo = photo;
        photo.Owner = this;
    }
}

please note that I have defined a method to assign a photo to the person. This method takes care of the fact that the relation between the person and the photo entity is bi-directional (via the assignment photo.Owner = this). I have omitted any validation code for brevity.

Let's also have a look at the implementation of the PersonPhoto class which I kept even simpler

public class PersonPhoto
{
    public virtual Guid Id { get; set; }
    public virtual Person Owner { get; set; }
    public virtual byte[] Image { get; set; }
}

Let's now define the mapping for my simple model.

Mapping with XML

The question is now: how can I map this model to achieve the desired result that is

  • whenever I load a person its photo is not automatically loaded but only on request (lazy load)
  • treating the person photo like an associated entity of the person entity, that is the photo is created, updated and deleted together with its parent - the person.

It is not possible in NHibernate to define a (bi-directional) one-to-one relation between Person and PersonPhoto and requesting at the same time that the PersonPhoto is lazy loaded.

But a possible solution to declare the relation between Person and PersonPhoto as many-to-one and between PersonPhoto and Photo as one-to-one.

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="Blobs"
                   namespace="Blobs">
  <class name="Person">
    <id name="Id">
      <generator class="guid"/>
    </id>
    <property name="LastName"/>
    <property name="FirstName"/>
    <many-to-one name="Photo" class="PersonPhoto" unique="true"
                 column="PersonPhotoId" cascade="all-delete-orphan"/>
  </class>
  
  <class name="PersonPhoto">
    <id name="Id">
      <generator class="guid"/>
    </id>
    <property name="Image" type="BinaryBlob"/>
    <one-to-one name="Owner" property-ref="Photo" constrained="true"/>
  </class>
</hibernate-mapping>

It is important to note that the many-to-one relation defined in the Person mapping is declared as unique to 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 should 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 indeed 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 use the SchemaExport class of NHibernate 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 should 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

DbSchemaGenerationTest

The script is generated for the SQLite database I use. For a different type of database server the script would 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 to 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 should 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: All the ShouldXXX methods 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 NHibernate --> 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 should 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 expect 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 calling the save method of the session but only when the session is flushed (that is: the insert command is executed on the database). I clear the session in the catch block since otherwise the exception would 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 previously save person from database and with the aid of a utility class of NHibernate I check whether the property Photo of 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 NHibernate

As described in earlier posts (part 1, part 2, part 3 and part 4) we have the possibility to use Fluent NHibernate to map our entities. Mapping this way has many 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 generic ClassMap<T> base class provided by the Fluent NHibernate 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 many-to-one relation between person and person photo is mapped with the aid of the References method.

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 to person) is mapped with the aid of the HasOne method.

Unit testing the Fluent NHibernate 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 using the Fluent NHibernate framework. 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 would not be possible to only implement and uni-directional realtion between person and person photo.

The answer is: there is NO special 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

unidirectional_model

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 NHibernate)

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值