Entity Framework 6 (7) vs NHibernate 4: DDD perspective(纯净DDD很难很难...)

There is quite a bit of Entity Framework vs NHibernate comparisons on the web already, but all of them cover mostly the technical side of the question. In this post, I’ll compare these two technologies from a Domain Driven Design (DDD) perspective. I’ll step through several code examples and show you how both of these ORMs let you deal with problems.

 

Onion architecture

Onion architecture

Nowadays, it is common to use onion architecture for complex systems. It allows you to isolate your domain logic from other pieces of your system so that you can focus your effort on the problems that are essential to your application.

Domain logic isolation means that your domain entities should not interact with any classes except other domain entities. It’s one of the core principles you should follow to make your code clean and coherent. Of course, it’s not always possible to achieve that level of isolation but it’s certainly worth trying.

The picture below shows onion architecture using the classic n-tier scheme.

Onion architecture from the N-tier perspective

Persistence Ignorance

While using an ORM, it is vital to sustain a good degree of persistence ignorance. It means that your code should be structured in such a way that allows you to factor the persistence logic out of your domain entities. Ideally, domain classes should not contain any knowledge about how they are being persisted. That allows us to adhere to the Single Responsibility Principle and thus keep the code simple and maintainable.

If you tend to write code like in the sample below, you are on the wrong path:

public class MyEntity

{

    // Perstisted in the DB

    public int Id { get; set; }

    public string Name { get; set; }

 

    // Not persisted

    public bool Flag { get; set; }

}

If your domain logic is separated from persistence logic, you can say that your entities are persistence ignorant. That means that you can change the way you persist their data without affecting the domain logic. Persistence ignorance is prerequisite to the domain logic isolation.

Case #1: Deleting an entity from aggregate root

Let’s go through the real-world code examples.

Order aggregate

Here is an aggregate that contains two classes at the diagram above. Order class is the aggregate root, which means that Order controls the Lines collection’s lifespan. If an Order is deleted, its Lines are deleted with it; OrderLine has no meaning without an Order.

Let’s assume that we want to implement a method that deletes a single line from an order. Here is how it can be implemented if you don’t have to save your entities in the database (i.e. without any ORM):

public ICollection<OrderLine> Lines { get; private set; }

public void RemoveLine(OrderLine line)

{

    Lines.Remove(line);

}

You just delete it from the collection, and that’s it. As long as Order is the root of the aggregate, any other classes must retrieve an Order’s instance in order to get access to its lines. If there’s no such line, we can say that it is deleted.

Now, if you try to do it with Entity Framework, you’ll get an exception:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable.

There’s no way to instruct Entity Framework to delete orphaned items from the database. You need to do that yourself:

public virtual ICollection<OrderLine> Lines { get; set; }

public virtual void RemoveLine(OrderLine line, OrdersContext db)

{

    Lines.Remove(line);

    db.OrderLines.Remove(line);

}

Passing OrdersContext parameter to the entity’s method breaks the Separation of Concerns principles, because the Order class becomes aware of how it is stored in the database.

Here’s how it can be done in NHibernate:

public virtual IList<OrderLine> Lines { get; protected set; }

public virtual void RemoveLine(OrderLine line)

{

    Lines.Remove(line);

}

Note that the code is almost identical to the code that is not bound to any ORM. You can instruct NHibernate to delete the orphaned line from the database by using this mapping:

public class OrderMap : ClassMap<Order>

{

    public OrderMap()

    {

        Id(x => x.Id);

        HasMany(x => x.Lines).Cascade.AllDeleteOrphan().Inverse();

    }

}

Case #2: Link to a related entity

Order line reference

Let’s say you need to introduce a link to a related entity. Here’s a code that is not bound to any ORM:

public class OrderLine

{

    public Order Order { get; private set; }

    // Other members

}

This is the default way to do that with Entity Framework:

public class OrderLine

{

    public virtual Order Order { get; set; }

    public int OrderId { get; set; }

    // Other members

}

The default way to do that in NHibernate:

public class OrderLine

{

    public virtual Order Order { get; set; }

    // Other members

}

As you can see, by default, Entity Framework requires an additional Id property to work with the reference. This approach violates SRP because Ids represent an implementation detail of how entities are stored in a database. Entity Framework impels you to work directly with the database’s concepts whereas a better approach in this situation would be just a single Order property.

Moreover, this code breaks the DRY principle. Declaring both OrderId and Order properties enables OrderLine class to easily fall into an inconsistent state:

Order = order; // An order with Id == 1

OrderId = 2;

Now, EF does allow for declaring a single Order property instead. But still, there are two problems with it:

  • Accessing the Id property of the related entity leads to loading that entity from the database, although that Id is already in memory. In contrary, NHibernate is smart enough to not do that. With NHibernate, the loading would be executed only if you refer to an Order’s property other than Id.
  • Entity Framework impels developers to use Ids instead of just related entity reference. Partly, because it’s the default way of doing this sort of things, and partly because all the examples use this approach. In contrast to Entity Framework, the default way to create a reference in NHibernate is the first one.

Case #3: Read-only collection of related entities

If you need to make the lines collection read-only for the Order’s clients, you could write this code (not bound to any ORM):

private List<OrderLine> _lines;

public IReadOnlyList<OrderLine> Lines

{

    get { return _lines.ToList(); }

}

The official way of doing it in EF:

public class Order

{

    protected virtual ICollection<OrderLine> LinesInternal { get; set; }

    public virtual IReadOnlyList<OrderLine> Lines

    {

        get { return LinesInternal.ToList(); }

    }

 

    public class OrderConfiguration : EntityTypeConfiguration<Order>

    {

        public OrderConfiguration()

        {

            HasMany(p => p.LinesInternal).WithRequired(x => x.Order);

        }

    }

}

public class OrdersContext : DbContext

{

    protected override void OnModelCreating(DbModelBuilder modelBuilder)

    {

        modelBuilder.Configurations.Add(new Order.OrderConfiguration());

    }

}

Clearly, this is not how you’d like to build your domain entities as you directly include an infrastructure class in your domain class. Unofficial solution is hardly better:

public class Order

{

    public static Expression<Func<Order, ICollection<OrderLine>>> LinesExpression =

        f => f.LinesInternal;

 

    protected virtual ICollection<OrderLine> LinesInternal { get; set; }

    public virtual IReadOnlyList<OrderLine> Lines

    {

        get { return LinesInternal.ToList(); }

    }

}

 

public class OrdersContext : DbContext

{

    protected override void OnModelCreating(DbModelBuilder modelBuilder)

    {

        modelBuilder.Entity<Order>()

        .HasMany(Order.LinesExpression);

    }

}

Still, you need to include infrastructure code in the Order class. Entity Framework doesn’t offer anything to factor this logic out.

Here’s how it can be done with NHibernate:

public class Order

{

    private IList<OrderLine> _lines;

    public virtual IReadOnlyList<OrderLine> Lines

    {

        get { return _lines.ToList(); }

    }

}

public class OrderMap : ClassMap<Order>

{

    public OrderMap()

    {

        HasMany<OrderLine>(Reveal.Member<Order>(“Lines”))

        .Access.CamelCaseField(Prefix.Underscore);

    }

}

Again, the way we do it with NHibernate is almost identical to the way we would do it without any ORM whatsoever. The Order class here is clean and doesn’t contain any persistence logic, and thus allows us to focus on the domain, dealing with one problem at a time.

NHibernate’s approach has one drawback, though. This code is vulnerable to refactoring as the property’s name is typed in as a string. I consider it to be a reasonable trade-off because it allows us to better separate the domain code from the persistence logic. Besides, such errors are really easy to detect either by manual testing or integration auto tests.

Case #4: Unit of Work pattern

Here’s another example. Below is the code from a task I worked on a couple years ago. I’ve omitted details for brevity, but you should get the point:

public IList<int> MigrateCustomers(IEnumerable<CustomerDto> customerDtos,

    CancellationToken token)

{

    List<int> ids = new List<int>();

 

    using (ISession session = CreateSession())

    using (ITransaction transaction = session.BeginTransaction())

    {

        foreach (CustomerDto dto in customerDtos)

        {

            token.ThrowIfCancellationRequested();

 

            Customer customer = CreateCustomer(dto);

            session.Save(customer);

 

            ids.Add(customer.Id);

        }

 

        transaction.Commit();

    }

 

    return ids;

}

The method takes some data, converts it to domain objects and saves them after that. It returns a list of customers’ Ids. The caller code has an ability to cancel the migration process using CancellationToken which is passed in as well.

If you use Entity Framework for this task, it will insert your entities in the database in order to get their Ids because, for integer identifiers, EF doesn’t allow for choosing any id generation strategy other than database identity. This approach works well for most of the cases, but it has a major flaw – it breaks the Unit of Work pattern. If the method is canceled, EF would have to delete all the records that have been inserted so far, which itself causes massive performance impact.

With NHibernate, you can choose Hi/Lo id generation strategy so that customers are simply not saved until the session is closed. Ids are generated on the client side, so there’s no need to touch the database to retrieve them. NHibernate can save a huge amount of time with this type of tasks.

Case #5: Working with cached objects

Let’s say your customer list is pretty stable and don’t change often. You may decide to cache them so that you can use customer list without querying the database. Now, let’s also say that the Order class has a precondition to belong to one of the customers. This precondition can be implemented using a constructor:

public Order(Customer customer)

{

    Customer = customer;

}

This way, we are sure that no order can be created without a customer.

With Entity Framework, you can’t set a detached object as a reference to a new object. If you use a constructor like the one above, EF will try to insert the customer in the database because it wasn’t attached to the current context. To fix it, you need to explicitly specify that the customer is already in the database:

public Order(Customer customer, OrdersContext context)

{

    context.Entry(customer).State = EntityState.Unchanged;

    Customer = customer;

}

Again, such approach breaks SRP. In contrast, NHibernate can detect an object’s state by its Id and doesn’t try to insert this object if it’s already in the database, so the NHibernate’s version would be the same as the non-ORM one.

Entity Framework vs NHibernate: results

There’s one simple method of how to measure an ORM’s persistence ignorance degree. The closer the code that uses an ORM is to a code that isn’t bound to any database, the cleaner this ORM, and the more it is persistence ignorant.

EF is too tightly coupled to the database notions. When you model your application with Entity Framework, you still need to think in terms of foreign-key constraints and table relationships. Without a clean and isolated domain model, your attention is constantly distracted, you can’t focus on the domain problems that are essential for your software.

The trick here is that you actually may not notice such distractions until your system grows in size. And when it does, it becomes really hard to sustain the pace of development because of increased maintainability costs.

That said, NHibernate is still way ahead of Entity Framework. Besides the better separation of concerns, NHibernate has a bunch of useful features that Entity Framework doesn’t: 2nd level cache, concurrency strategies, rich mapping capabilities and so on and so forth. Probably, the only feature that EF can boast of is async database operations.

Why is it so? Isn’t Entity Framework being actively developed for many years? EF was initially created as a tool for working on simple CRUD applications and only several years after got some really good features, such as code-first mapping. It seems that this initial purpose is still leaking out, even after all these years of active development. It means that while Entity Framework is a good tool in many situations, if you want to create a domain model which fully adheres to the DDD principles, you are still better off choosing NHibernate.

I believe Entity Framework can replace NHibernate eventually, but the EF team will need to adjust its approach to building the ORM in order to achieve it (although, admittedly, they did a very good job since the version 1).

Update 12/1/2014

The Entity Framework program manager Rowan Miller replied that some of this issues will be addressed in EF7, although, still, not all of them. Nevertheless, EF seems to be going in a right direction, so let’s cross our fingers.

 If you want to learn more about how to build a rich domain model having an ORM at hand, you can watch my Pluralsigh course Domain-Driven Design in Practice.

from:http://enterprisecraftsmanship.com/2014/11/29/entity-framework-6-7-vs-nhibernate-4-ddd-perspective/

posted @ 2017-02-09 17:26  遥望星空  阅读(365)  评论(0编辑  收藏  举报