Aggregate lists vs Queries
ALL THE EXAMPLES FROM THIS POST ARE EXTRACTED FROM THE FWKLIGHT DEMO. TO FIND OUT HOW TO GET STARTED WITH THE DEMO, READ THIS POST.
Loading lists of aggregates is not the same with loading a list inside an aggregate.
For example, if CustomerOrder is an aggregate and the business of the application suggests that it should contain a list of OrderDetails, you might load a list of CustomerOrders with a query at some point, but you should not use a query to load the list of OrderDetails for a CustomerOrder. Instead, to obtain the list of OrderDetails for a CustomerOrder you should load the CustomerOrder with a repository (or load a list of CustomerOrders with a query), and ask the CustomerOrder for its OrderDetails.
Here is how these entities and their mappings look like:
public class OrderDetail1 { public virtual int Id { get; set; } public virtual decimal OrderDetailPrice { get; set; } public virtual CustomerOrder1 CustomerOrder1 { get; set; } }
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" schema="dbo" assembly="FwkLightDemo.Domain" namespace="FwkLightDemo.Domain.Entities"> <class name="OrderDetail1" table="OrderDetail1"> <id name="Id" type="Int32" column="OrderDetail1Id" > <generator class="identity" /> </id> <many-to-one name="CustomerOrder1" column="CustomerOrder1Id" class="CustomerOrder1" not-null="true" /> <property name="OrderDetailPrice"/> </class> </hibernate-mapping>
public class CustomerOrder1 { public virtual int Id { get; set; } public virtual decimal OrderPrice { get; set; } public virtual IList<OrderDetail1> OrderDetails { get; set; } }
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" schema="dbo" assembly="FwkLightDemo.Domain" namespace="FwkLightDemo.Domain.Entities"> <class name="CustomerOrder1" table="CustomerOrder1"> <id name="Id" type="Int32" column="CustomerOrder1Id" > <generator class="identity" /> </id> <property name="OrderPrice"/> <bag name="OrderDetails" inverse="true"> <key column="CustomerOrder1Id" /> <one-to-many class="OrderDetail1" /> </bag> </class> </hibernate-mapping>
In the QueryController we have the AggregateLists action, where:
- we first load a list of CustomerOrders, select the one for which we need to retrieve the OrderDetails and ask it for its OrderDetails:
var customerOrderList = _aggregateListTask1.Execute(2); var orderForWhichWeRetrieveDetails = customerOrderList.FirstOrDefault(p => p.Id == 3); var orderDetailListGood = orderForWhichWeRetrieveDetails.OrderDetails;
This is the efficient way to access the list of OrderDetails for a CustomerOrder, and the efficient way to access a list of subentities inside an aggregate.
- after that, we obtain the same list of OrderDetails by executing a query specialized in retrieving the OrderDetails for a CustomerOrder:
var orderDetailListBad = _aggregateListTask2.Execute(orderForWhichWeRetrieveDetails);
The AggregateListTask2 QueryTask is simply a wrapper for AggregateListQuery2:
public class AggregateListQuery2: MultiCriteriaQuery<OrderDetail1> { private CustomerOrder1 _order; public IList<OrderDetail1> Execute(ISession session, CustomerOrder1 order) { _order = order; return Load(session); } protected override void ApplyMainFilters(IListMultiCriteriaFetcher<OrderDetail1> criteria) { criteria.Where(p => p.CustomerOrder1).Eq(_order); } protected override void FilterOnlyActive(IListMultiCriteriaFetcher<OrderDetail1> criteria) { } protected override void Sort(IListMultiCriteriaFetcher<OrderDetail1> criteria) { } }
public class AggregateListTask2 : BaseListQueryTask<OrderDetail1> { private readonly AggregateListQuery2 _aggregateListQuery2; private CustomerOrder1 _order; public AggregateListTask2(INHUnitOfWorkProvider uowProvider, AggregateListQuery2 aggregateListQuery2) : base(uowProvider) { _aggregateListQuery2 = aggregateListQuery2; } public IList<OrderDetail1> Execute(CustomerOrder1 order) { _order = order; ExecuteInternal(); return Entity; } protected override IList<OrderDetail1> ExecuteQuery() { return _aggregateListQuery2.Execute(UOW.Session, _order); } }
This is the complicated way to access the list of OrderDetails for a CustomerOrder, and the complicated way to access a list of subentities inside an aggregate. By using this method you work more (you create at least 2 more classes), only to obtain code that is harder to control and maintain.
You should use the efficient method as often as you can, but there are a few things to consider.
When to favor loading lists inside aggregates over dedicated queries
We first have to make an observation: for one context an entity must be considered an aggregate (it cannot be loaded inside any of the entities which are loaded in that context), and for another context the same entity can be considered a subentity (it can be loaded inside one of the other entities which are loaded in that context). This subtle difference is dictated by the business of the application and by each functionality / context that we are implementing, and making good use of it is essential.
As an example, let’s consider that a CustomerOrderSet can contain 1-100 CustomerOrders. If we need to show the list with all CustomerOrders, then we are only talking about the orders so there is no aggregate inside which we could load them – CustomerOrder is the aggregate. If we need to show the list of CustomerOrders for a CustomerOrderSet, then we have a context for the CustomerOrders, the CustomerOrderSet – CustomerOrderSet is the aggregate, and we can consider CustomerOrder its subentity.
Here is how we use this observation:
- when the entity must be considered an aggregate, we always load the single instance with a repository and the list with a query
- when the entity can be considered a subentity, the simplified rule is: whenever you can load the list inside an aggregate without too much pain, do it
Here is how the detailed rule looks:
- if you are implementing a list with a pager and / or filters, you should load its contents with a query (no aggregate should take responsibility for filtering / paging through one of its lists); you should also consider using a linear entity (which is more appropriate for displaying lists) instead of a normal entity, but that’s another story
- if you need to load a big list of entities (>1000) or a list which you expect to grow very fast, you should use a query (such a list should be cached, and if displayed in the UI it should be filtered or paged – none of these are responsibilities for an aggregate; because of its size, such a list should never be included inside an aggregate because it has the potential to create performance bottlenecks – loading it with a dedicated query isolates it and reduces the risks)
- if the aggregate inside which you can load the subentities must be loaded for the current context / page, you should load the list inside the aggregate
- otherwise, you should use a query
Although there is only one case in which you should load the list inside the aggregate, it’s the one that happens most.
As an example, in the ListsInsideAggregatesOverQueries action inside the QueryController:
- we are first loading a list with all the CustomerOrders by using an AllItemsTask (which uses a query for loading all entities of a certain type):
var ordersWhenAggregateOk = _customerOrder3ListTask.Execute();
Here is how the entity and mapping looks like:
public class CustomerOrder3 { public virtual int Id { get; set; } public virtual decimal OrderPrice { get; set; } public virtual CustomerOrderSet3 CustomerOrderSet3 { get; set; } }
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" schema="dbo" assembly="FwkLightDemo.Domain" namespace="FwkLightDemo.Domain.Entities"> <class name="CustomerOrder3" table="CustomerOrder3"> <id name="Id" type="Int32" column="CustomerOrder3Id" > <generator class="identity" /> </id> <property name="OrderPrice"/> <many-to-one name="CustomerOrderSet3" column="CustomerOrderSet3Id" class="CustomerOrderSet3" not-null="true" /> </class> </hibernate-mapping>
This is a case in which CustomerOrder is the aggregate, so it’s ok to load it separately with its own query.
- after that, we load a CustomerOrderSet with a LoadTask, and its CustomerOrders separately with a QueryTask:
var customerOrderSet = _customerOrderSet3LoadTask.Execute(1); var ordersWhenNotAggregateBad = _customerOrder3ListTask2.Execute(customerOrderSet);
Here is the CustomerOrderSet entity looks like:
public class CustomerOrderSet3 { public virtual int Id { get; set; } public virtual decimal OrderSetPrice { get; set; } public virtual IList<CustomerOrder3> CustomerOrders { get; set; } }
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" schema="dbo" assembly="FwkLightDemo.Domain" namespace="FwkLightDemo.Domain.Entities"> <class name="CustomerOrderSet3" table="CustomerOrderSet3"> <id name="Id" type="Int32" column="CustomerOrderSet3Id" > <generator class="identity" /> </id> <property name="OrderSetPrice"/> <bag name="CustomerOrders" inverse="true"> <key column="CustomerOrderSet3Id" /> <one-to-many class="CustomerOrder3" /> </bag> </class> </hibernate-mapping>
CustomerOrderSet3LoadTask is a simple LoadTask:
public class CustomerOrderSet3LoadTask : BaseLoadTask<CustomerOrderSet3> { private int _id; public CustomerOrderSet3LoadTask(IBaseRepo<CustomerOrderSet3> repository, INHUnitOfWorkProvider uowProvider) : base(repository, uowProvider) { } public CustomerOrderSet3 Execute(int id) { _id = id; ExecuteInternal(); return Entity; } protected override void ConfigureFilters() { Fetcher.Where(p => p.Id).Eq(_id); } }
CustomerOrder3Query2 is a query which loads the orders for an orderSet:
public class CustomerOrder3Query2: MultiCriteriaQuery<CustomerOrder3> { private CustomerOrderSet3 _customerOrderSet3; public IList<CustomerOrder3> Execute(ISession session, CustomerOrderSet3 customerOrderSet3) { _customerOrderSet3 = customerOrderSet3; return Load(session); } protected override void ApplyMainFilters(IListMultiCriteriaFetcher<CustomerOrder3> criteria) { criteria.Where(p => p.CustomerOrderSet3).Eq(_customerOrderSet3); } protected override void FilterOnlyActive(IListMultiCriteriaFetcher<CustomerOrder3> criteria){} protected override void Sort(IListMultiCriteriaFetcher<CustomerOrder3> criteria){} }
and the CustomerOrder3ListTask2 is a wrapper for it:
public class CustomerOrder3ListTask2: BaseListQueryTask<CustomerOrder3> { private readonly CustomerOrder3Query2 _customerOrder3Query; private CustomerOrderSet3 _customerOrderSet3; public CustomerOrder3ListTask2(INHUnitOfWorkProvider uowProvider, CustomerOrder3Query2 customerOrder3Query) : base(uowProvider) { _customerOrder3Query = customerOrder3Query; } public IList<CustomerOrder3> Execute(CustomerOrderSet3 customerOrderSet3) { _customerOrderSet3 = customerOrderSet3; ExecuteInternal(); return Entity; } protected override IList<CustomerOrder3> ExecuteQuery() { return _customerOrder3Query.Execute(UOW.Session, _customerOrderSet3); } }
This is a case in which CustomerOrder can be considered a subentity (because we have an aggregate inside which we can load it – the CustomerOrderSet). We are not showing a list with a pager or filters, it’s not a big list (max 100) and it does not grow in time, so it’s not ok to load it separately with its own query like we did. Instead, we should load it together with the CustomerOrderSet aggregate.
- next, we obtain the same list of CustomerOrders directly from the CustomerOrderSet:
var ordersWhenNotAggregateGood = customerOrderSet.CustomerOrders;
Considering the conditions that we just described, this is the right approach.
About this entry
You’re currently reading “Aggregate lists vs Queries,” an entry on The FwkLight blog
- Published:
- April 26, 2010 / 10:54 am
- Category:
- Uncategorized
- Tags:
No comments yet
Jump to comment form | comment rss [?] | trackback uri [?]