Sunday, December 8, 2013

Pragmatic Domain-Driven Design

There's a DDD, then there's a pragmatic DDD. When we say pragmatic, it means the application must not sacrifice performance


You wanted to get the count of the person's hobbies and you wanted your code to be performant.

public class Person
{
    public virtual int PersonId { get; set; }    
    public virtual string Name { get; set; }
    
    public virtual IList<Hobby> Hobbies { get; set; }
}


This is not DDD. DDD must encapsulate, if we access Hobbies count directly we "can't" add any further condition(say only count the active hobbies) on it and expect the code to be performant, see further comments below.

var p = session.Load<Person>(1);
Console.WriteLine("Count: {0}", p.Hobbies.Count());


This is DDD, intelligence are encapsulated by the domain model. And this is also performant, the list is not eagerly-loaded, the count is performed directly by the database

public virtual int FavoriteHobbiesCount
{
    get
    {
        // Thanks Extra Lazy! This is on PersonMapping:
        //    rel.Lazy(NHibernate.Mapping.ByCode.CollectionLazy.Extra);

        // With Extra Lazy, counting will be performed at the database-side instead of counting the in-memory objects
        return this.Hobbies.Count();
    }
}

// On Main()
var p = session.Load<Person>(1);
var count = p.FavoriteHobbiesCount;
Console.WriteLine("Count: {0}", count);    

Output:


NHibernate:
    SELECT
        person0_.person_id as person1_0_0_,
        person0_.first_name as first2_0_0_,
        person0_.last_name as last3_0_0_
    FROM
        person person0_
    WHERE
        person0_.person_id=:p0;
    :p0 = 1 [Type: Int32 (0)]
NHibernate:
    SELECT
        count(favorite_hobby_id)
    FROM
        favorite_hobby
    WHERE
        person_id=:p0;
    :p0 = 1 [Type: Int32 (0)]
Count: 10





However, that code is not future-proof, Extra Lazy won't work efficiently when you add a condition on the list. i.e., the collection will be eagerly-loaded when you add a condition on it.

public virtual int FavoriteHobbiesCount
{
    get
    {
        // Thanks Extra Lazy! This is on PersonMapping
        //     rel.Lazy(NHibernate.Mapping.ByCode.CollectionLazy.Extra);

        // Hobbies' items will be eagerly-loaded when we add a condition on its Count even we use Extra Lazy
        return this.Hobbies.Count(x => x.IsActive); 
    }
}

// On Main()
var p = session.Load<Person>(1);
var count = p.FavoriteHobbiesCount;
Console.WriteLine("Count: {0}", count);    

Output:

NHibernate:
    SELECT
        person0_.person_id as person1_0_0_,
        person0_.first_name as first2_0_0_,
        person0_.last_name as last3_0_0_
    FROM
        person person0_
    WHERE
        person0_.person_id=:p0;
    :p0 = 1 [Type: Int32 (0)]
NHibernate:
    SELECT
        favoriteho0_.person_id as person2_1_,
        favoriteho0_.favorite_hobby_id as favorite1_1_,
        favoriteho0_.favorite_hobby_id as favorite1_1_0_,
        favoriteho0_.person_id as person2_1_0_,
        favoriteho0_.hobby as hobby1_0_,
        favoriteho0_.is_active as is4_1_0_
    FROM
        favorite_hobby favoriteho0_
    WHERE
        favoriteho0_.person_id=:p0;
    :p0 = 1 [Type: Int32 (0)]
Count: 9


The Count(x => x.IsActive) happens on application-side only, instead of being run on database.


To fix the slow performance, we must directly query the database. Pass an IQueryable to Person:


public virtual int GetFavoriteActiveHobbiesCountFromQueryable(IQueryable<FavoriteHobby> fh)
{            
    return fh.Count(x => x.Person == this && x.IsActive);            
}

    
// On Main()
var p = session.Load<Person>(1);
var count = p.GetFavoriteActiveHobbiesCountFromQueryable(s.Query<FavoriteHobby>()); 
Console.WriteLine("Count: {0}", count);    


Output:

NHibernate:
    SELECT
        person0_.person_id as person1_0_0_,
        person0_.first_name as first2_0_0_,
        person0_.last_name as last3_0_0_
    FROM
        person person0_
    WHERE
        person0_.person_id=:p0;
    :p0 = 1 [Type: Int32 (0)]
NHibernate:
    select
        cast(count(*) as int4) as col_0_0_
    from
        favorite_hobby favoriteho0_
    where
        favoriteho0_.person_id=:p0
        and favoriteho0_.is_active=TRUE;
    :p0 = 1 [Type: Int32 (0)]
Count: 9


However, you'll notice that even we don't access any of the property of Person, the Person model is still eagerly-loaded from database. NHibernate will eagerly-load the model when we access any of its properties/methods, regardless of being mapped or unmapped.


To really fix that slow performance, move the model's behavior to extension method:

public static class PersonMethodsWithPerformanceConcerns
{
    public static int GetActiveFavoriteHobbies(this Person p, IQueryable<FavoriteHobby> fh)
    {            
        Console.WriteLine("Extension method version");
        return fh.Count(x => x.Person == p && x.IsActive);
    }
}


// On Main()
var p = session.Load<Person>(1);
var count = p.GetActiveFavoriteHobbies(s.Query<FavoriteHobby>()); 
Console.WriteLine("Count: {0}", count);

Output:

Extension method version
NHibernate:
    select
        cast(count(*) as int4) as col_0_0_
    from
        favorite_hobby favoriteho0_
    where
        favoriteho0_.person_id=:p0
        and favoriteho0_.is_active=TRUE;
    :p0 = 1 [Type: Int32 (0)]
Count: 9


That's it, performance must not be compromised on the altar of DDD


Full code: https://github.com/MichaelBuen/TestAggregate


Update 2018-May-20

On NHibernate 5, the collection will not be eager-loaded anymore when adding a condition on collection. It will perform a real database query instead. Prior to 5, this:

public virtual int FavoriteHobbiesCount
{
    get
    {
        // Thanks Extra Lazy! This is on PersonMapping
        //     rel.Lazy(NHibernate.Mapping.ByCode.CollectionLazy.Extra);

        // Hobbies' items will be eagerly-loaded when we add a condition on its Count even we use Extra Lazy
        return this.Hobbies.Count(x => x.IsActive); 
    }
}

will run this query, and perform the Count on application instead.
NHibernate:
    SELECT
        favoriteho0_.person_id as person2_1_,
        favoriteho0_.favorite_hobby_id as favorite1_1_,
        favoriteho0_.favorite_hobby_id as favorite1_1_0_,
        favoriteho0_.person_id as person2_1_0_,
        favoriteho0_.hobby as hobby1_0_,
        favoriteho0_.is_active as is4_1_0_
    FROM
        favorite_hobby favoriteho0_
    WHERE
        favoriteho0_.person_id=:p0;
    :p0 = 1 [Type: Int32 (0)]


With NHibernate 5, just add AsQueryable() on an entity's collection property, and NHibernate will happily run the query from the database instead, even if there's a condition on collection's query.


public virtual int FavoriteHobbiesCount
{
    get
    {
        // Hobbies' items will not be be eagerly-loaded anymore on NHibernate 5 even when we add a condition on its Count.
        return this.Hobbies.AsQueryable().Count(x => x.IsActive); 
    }
}

The resulting query would be like:
NHibernate:
    SELECT
        cast(count(*) as int4) as col_0_0_
    FROM
        favorite_hobby favoriteho0_
    WHERE
        favoriteho0_.person_id=:p0
        and favoriteho0_.is_active=:p1;
    :p0 = 1 [Type: Int32 (0)], :p1 = true [Type: Boolean]


Happy Computing! ツ

No comments:

Post a Comment