Queries and calculated primitives
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.
By primitive we are referring to a boolean, an int, a decimal, etc. You sometimes need to apply a formula to calculate a primitive, and you have the option of doing it in the domain or in the database.
To let the database apply the formula and return the result, you need to create a query which makes the necessary database call. Here is an example of a query which calculates the maximum discount percentage that a client can receive, based on a list of discount levels and the client type. The query uses HQL (Hibernate Query Language) to apply the formula in the HQLForActiveItems method:
public class CalculatedPrimitiveQuery1 : HqlQuery<Client2, int> { private Client2 _client; public decimal Execute(ISession session, Client2 client) { _client = client; return LoadUnique(session); } protected override string HQLForActiveItems { get { return @"select max(dl.DiscountPercentage) from DiscountLevel2 as dl where dl.DiscountPercentage <= (select max(dl1.DiscountPercentage) from DiscountLevel2 as dl1 where dl1.ClientTypeWithLimit = :ClientType or not exists (select dl1.DiscountPercentage from DiscountLevel2 as dl1 where dl1.ClientTypeWithLimit = :ClientType) )"; } } protected override string HQLForAllItems { get { throw new NotImplementedException(); } } public override void SetParameters(IQuery query) { query.SetInt32("ClientType", (int)_client.ClientType); } protected override string Sorting { get { return ""; } } }
Most of the times when you seem to need a query with this purpose, the calculation should be done inside one of the existing aggregates or one that you did not identify yet. The fact that the aggregate is not used is already a problem, and isolating the calculation in a query adds more to it.
When you isolate a calculation inside a query, you make a decision that the calculation will always be needed directly from the database and you give it a form that is database-related (as you can see in the example above); in a way, you put that formula in the database instead of the domain and you lose the flexibility that the domain offers. As soon as the calculation will be needed inside the domain, you will face a duplication of logic (the calculation will be written once more, this time in the domain) or a painful refactor (to extract the calculation from the query, into a form that both the query and domain can use).
Plus, you are giving the query 2 responsibilities: the classical responsibility of loading something from the database, plus the responsibility of being the keeper of that business formula; both these responsibilities are heavy, so it’s best to not mix them.
It’s good to avoid these problems altogether, and use queries only for loading lists of aggregates as much as you can.
In conclusion, when you seem to need a query which loads a primitive from the database, use this simple guideline:
- if the formula must be applied over a big list of entities (>1000) or a list which you expect to grow very fast (today it will be applied on 100 database entries, in one month it will be applied on 1000, and so on), use a query to apply the formula
- otherwise, apply the formula in the domain
- what makes sense to be in the aggregate – load with the aggregate
- what does not make sense to be in the aggregate – load independently
- create an aggregate extension which uses the aggregate and the other dependencies as parameters, to apply the formula and calculate the primitive
In the CalculatedPrimitives action of the QueryController:
- we first load the client:
var client = _client2LoadTask.Execute(1);
- after that, we calculate the maximum discount percentage that the client can receive using the above query:
var maxDiscountPercentageBad = _calculatedPrimitiveTask1.Execute(client);
CalculatedPrimitiveTask1 is simply a wrapper for the CalculatedPrimitiveQuery1:
public class CalculatedPrimitiveTask1 : BaseQueryTask<Client2, decimal> { private readonly CalculatedPrimitiveQuery1 _calculatedPrimitiveQuery1; private Client2 _client; public CalculatedPrimitiveTask1(INHUnitOfWorkProvider uowProvider, CalculatedPrimitiveQuery1 calculatedPrimitiveQuery1) : base(uowProvider) { _calculatedPrimitiveQuery1 = calculatedPrimitiveQuery1; } public decimal Execute(Client2 client) { _client = client; ExecuteInternal(); return Entity; } protected override decimal ExecuteQuery() { return _calculatedPrimitiveQuery1.Execute(UOW.Session, _client); } }
As we have seen in the CalculatedPrimitiveQuery1 query, we will end up with a complicated HQL formula which will surely bring problems in the long run.
- in the second example, we make the same calculation but this time in the domain:
var maxDiscountPercentageGood = _calculatedPrimitiveTask2.Execute(client);
This solution uses the guideline that we presented earlier, because the task loads the list of discount levels separately in the LoadAdditionalInitialData method:
public class CalculatedPrimitiveTask2 : BaseApplicationTask<decimal> { private readonly DiscountLevel2ListTask _discountLevel2ListTask; private readonly MaxDiscountPercentage _maxDiscountPercentage; private Client2 _client; private IList<DiscountLevel2> _discountLevel2List; public CalculatedPrimitiveTask2(INHUnitOfWorkProvider uowProvider, DiscountLevel2ListTask discountLevel2ListTask, MaxDiscountPercentage maxDiscountPercentage) : base(uowProvider) { _discountLevel2ListTask = discountLevel2ListTask; _maxDiscountPercentage = maxDiscountPercentage; } public decimal Execute(Client2 client) { _client = client; ExecuteInternal(); return Entity; } protected override void LoadAdditionalInitialData() { _discountLevel2List = _discountLevel2ListTask.Execute(); } protected override void ExecuteBusinessLogic() { Entity = _client.CalculateThe(_maxDiscountPercentage, _discountLevel2List); } }
and the formula is encapsulated in an entity extension (a Calculator) which is executed in the ExecuteBusinessLogic method:
public class MaxDiscountPercentage : ICalculator<decimal, Client2, IList<DiscountLevel2>> { public decimal Calculate(Client2 client, IList<DiscountLevel2> discountLevels) { var maxLevel = discountLevels.FirstOrDefault(p => p.ClientTypeWithLimit == client.ClientType); if (maxLevel != null) return maxLevel.DiscountPercentage; return discountLevels.Max(p => p.DiscountPercentage); } }
With this solution, everything remains in the domain, nicely encapsulated, and we don’t lose any flexibility.
About this entry
You’re currently reading “Queries and calculated primitives,” an entry on The FwkLight blog
- Published:
- April 27, 2010 / 10:10 am
- Category:
- Uncategorized
- Tags:
No comments yet
Jump to comment form | comment rss [?] | trackback uri [?]