Reputation: 8115
Since all entities have a stamped of who created/modified the record, can we consider a Person entity an aggregate root to all entities?
That is, all entities that references the Person will become a collection to Person, e.g.
public class Person
{
public virtual int PersonId { get; set; }
public virtual string Lastname { get; set; }
public virtual IList<OrderHeader> CreatedOrders { get; set; }
public virtual IList<OrderHeader> ModifiedOrders { get; set; }
// Other entities that have a reference on Person will be mapped as a collection under
// the Person entity
}
public class OrderHeader
{
public virtual int OrderId { get; set; }
public virtual DateTime OrderDate { get; set; }
public virtual Customer Customer { get; set; }
public virtual string CommentsOnThisOrder { get; set; }
// stamp audit-level concerns
public virtual Person CreatedBy { get; set; }
public virtual DateTime DateCreated { get; set; }
public virtual Person ModifiedBy { get; set; }
public virtual DateTime DateModified { get; set; }
public virtual IList<OrderItem> OrderItems { get; set; }
}
public class OrderItem
{
public virtual OrderHeader OrderHeader { get; set; }
public virtual Product Product { get; set; }
public virtual int Quantity { get; set; }
public virtual decimal Price { get; set; }
}
That will basically make all entities become a collection to Person, which violates DDD aggregate root rules.
In my limited understanding of DDD aggregrate, the OrderHeader must not become a collection to Person, as we should not save the Order aggregate via Person. The only *entry* point for saving the Order aggregate(object graph) must be done from the OrderHeader, not from the Person.
Now here comes my real goal even why it looks dirty, I still wanted the Order to be a collection to Person:
There's one ORM *cough* NHibernate *cough* that cannot do a LEFT JOIN (.DefaultIfEmpty) from Person to OrderHeader if OrderHeader is not mapped as a collection to Person. The only way to achieve LEFT JOIN from Person to OrderHeader is to map the OrderHeader as a collection to Person.
Should I allow infrastructure concerns (e.g. facilitating LEFT JOIN from Person to OrderHeader via Linq's .DefaultIfEmpty by making the OrderHeader become a collection to Person) break the rules on when should only an entity must become an aggregate root?
If we will not map the OrderHeader as a collection to Person, then if we needed to make a LEFT JOIN (flattened result, not hierarchical, hence the need to use LEFT JOIN) from Person to OrderHeader on NHibernate, the only option left is to use QueryOver. QueryOver is very tedious to write compared to Linq.
Cannot use the .DefaultIfEmpty(LEFT JOIN functionality) on NHibernate's Linq if we manually do a LEFT JOIN (i.e., via Linq's join and .DefaultIfEmpty) from Person to OrderHeader, .DefaultIfEmpty throws an exception if we do a manual LEFT JOIN on NHibernate, LEFT JOIN on NHibernate's Linq must be done via collection and .DefaultIfEmpty.
If done occasionally(per need-basis), is breaking the rules on aggregate root a pragmatic choice? Example, should I map the OrderHeader as collection to Person in order to facilitate LEFT JOIN from Person to OrderHeader via Linq?
EDIT
Sample northwind database. When we need to report all customers with their orders including those customers without orders(e.g., PARIS)
CustomerID OrderID
OTTIK | 10407
OTTIK | 10684
OTTIK | 10554
PARIS |
PERIC | 10502
PERIC | 10474
PERIC | 10995
PERIC | 10354
PERIC | 11073
PERIC | 10322
, we need to do a LEFT JOIN:
select c.CustomerID, o.OrderID
from customers c
left join orders o on c.CustomerID = o.CustomerID
order by c.CustomerID
That can be done with Linq's join and .DefaultIfEmpty(). However NHibernate's Linq can't do a .DefaultIfEmpty on the result of manual Linq join, it throws an exception. NHibernate's DefaultIfEmpty can only be applied on a collection. But I feel mapping a collection to something that is not an aggregate root violates DDD aggregate root rules. And also doing so, all entities such as Person (Customer is another example) will potentially contain collections of ALL entities, as every table has a CreatedByPerson reference.
@DanielSchilling:
I'm also surprised(in a good way), that referencing the OrderHeader from OrderItem violates DDD. Do we have some sort of whitepaper or a Martin Fowler article expounding on that? I thought referencing a parent entity from a child entity is not considered an infrastructure concern, thus considered DDD.
Upvotes: 3
Views: 772
Reputation: 4977
It sounds like you want to be able to do this:
var people = session.Query<Person>()
.FetchMany(x => x.CreatedOrders)
.Where(x => x.Id == personId);
You should just flip the query around, like so...
var orders = session.Query<OrderHeader>()
.Fetch(x => x.CreatedBy)
.Where(x => x.CreatedBy.Id == personId);
There's more reasons than just DDD purity to keep person.CreatedOrders
out of your model. What if you have one person that places 10 orders a day, every day of the year, year after year? The first query almost certainly loads more data than you actually need. The second query, the one that doesn't need a person.CreatedOrders
collection at all, gives you more control over which orders to fetch and how many to fetch.
var orders = session.Query<OrderHeader>()
.Fetch(x => x.CreatedBy)
.Where(x => x.CreatedBy.Id == personId)
.Where(x => x.OrderDate >= DateTime.Now.AddYears(-1))
.Skip(100)
.Take(50);
Generally, if you find you're violating a DDD principle, it's usually a good indicator that you're doing something wrong, and you usually don't have to look too far to find other supporting evidence.
Every once in a while, though, you do have to bend the rules a bit, and I think that's acceptable. For example, I usually try to just map one side of each relationship - that's all I really need from a DDD perspective. When referencing other aggregate roots, that usually means just modeling the many-to-one
side, and for references within an aggregrate root - from the root to its children, that usually means just modeling the one-to-many
and leaving the many-to-one
out of the model. That would mean leaving OrderItem.OrderHeader
out of the model.
What if you needed to find out how much of a particular product had been sold, but only for Orders that have been Submitted? The easiest way to do this would be like so:
var quantitySold = session.Query<OrderItem>()
.Where(x => x.Product.Id == productId && x.OrderHeader.Submitted)
.Sum(x => x.Quantity);
So in this case, we need to model both the one-to-many
orderHeader.OrderItems
and also the many-to-one
orderItem.OrderHeader
. This is because it is most efficient for this SUM to access the relevant OrderItems directly, rather than first go through OrderHeader
as DDD would dictate. I'm OK with that.
Ignoring the DDD part of the question, this LEFT OUTER JOIN can be simulated by combining the results of two separate queries - one to get the orders...
var orders = session.Query<OrderHeader>()
.Fetch(x => x.CreatedBy);
... and another to get the people that have no orders:
var peopleWithNoOrders = session.Query<Person>()
.Where(p => !session.Query<OrderHeader>().Any(o => o.CreatedBy == p));
Upvotes: 5
Reputation: 13256
Your Person
may be an aggregate and your Order
may b an aggregate. One aggregate root instance should not reference an instance of another. Try not to think in terms of ORM or database.
I can point you to these answers (they may be relevant):
How to avoid holding reference to non-root-aggregate in another non-root-aggregate?
Upvotes: 1