In the past I’ve been using cborm a lot, since it makes handling coldfusion (hibernate) ORM so much easier. But lucee support for ORM was less than optimal in a multi-datasource environment, so I decided to rewrite this application more or less according to the fluent API approach as demonstrated by Gavin Pickin at ITB 2020. In this coding style I have two quite efficient ways of populating a new object:
property name="UserService" inject;
//populate
var user = populateModel(
model=UserService.new(),
memento=myUserData
);
//vs a shorter method
var user = UserService.new( myUserData );
Both should return the same populated user object, but the second one does the population within the new() method, so I got used to using this handy method.
Recently I inherited a project full of ORM entities, so I decided to use the latest (v2) version of cborm. Although I was quite happy with v1 I discovered v2 is a lot better with more fluent style coding, mementifier included and better documentation, especially on the very valuable but quite unknown criteria builder. In this project I also wanted to populate my entities. And cbOrm has virtual entity services, which can save me a lot of code writing for simple methods. So code looks like this:
property name="vUserService" inject="entityService:User"
//populate
var user = vUserService.populate( vUserService.new(), myUserData );
//vs a shorter method
var user = vUserService.new( myUserData );
As you can see, there’s only small differences with previous code. In this case I injected a virtual userService, so no coding of a real service at all.
Instead of the populateModel method I used the populate method on the virtual service. Main advantage: it has a slightly different arguments order, so we don’t have to specify named arguments when we only need model and memento data.
I was a happy coder, populated many new models according to the short method, until I was rewriting my User object. So what’s the difference with other entities?
My User object has something special. I injected a bCrypt reference and rewrote my setPassword() method so it would immediately hash my password. Something like this:
component accessors="true" persistent="true" table="users" {
// DI
property name="bcrypt" inject="BCrypt@BCrypt" persistent=false;
// more code....
function setPassword( value ){
variables.password =
variables.bcrypt.hashPassword( arguments.value );
}
//...
But now my vUserService.new( myUserData )
failed immediately, while vUserService.populate( vUserService.new(), myUserData )
was still OK. I didn’t understand why this should be different, since the new
() method just wraps a populate method, so I decided to investigate the source code:
any function new(
required string entityName,
struct properties = structNew(),
//.. more arguments
) {
var entity = entityNew( arguments.entityName );
// Properties exists?
if ( NOT structIsEmpty( arguments.properties ) ) {
populate(
target = entity,
memento = arguments.properties,
// .. more arguments
);
}
// Event Handling? If enabled, call the postNew() interception
if ( getEventHandling() ) {
getORMEventHandler().postNew( entity, arguments.entityName );
}
return entity;
}
And yes, there it is. First a new entity is created with the standard cfml entityNew()
method, followed by a populate. Finally it is calling postNew()
on the ORMEventHandler (only if enabled). The point here is that a new entity is NOT created by wirebox as in the non cborm code, so we need another way to process injections. That’s what the postNew
method in the OrmEventHandler does. It is a sort of ORM event bridge, but also processes wirebox injections and finally announces an OrmPostNew() interception.
So in my own code I try to populate the user password property. But although I have a new user entity, actually processing is not finished yet. When I try to set the password, the bcrypt reference is not there yet, since it is only there AFTER the postNew method finishes wirebox injection procession.
So how do we solve this? There is a very simple way: don’t use the new() method to populate entities if they have injections. So this method will never have this problems:
var user = vUserService.populate( vUserService.new(), myUserData );
This is a quite simple short time solution. For a long time solution we should look at the new() method in the entityService. We could move up the postNew() method so the wirebox injections are processed before the population of the object. But since the postNew is also announcing an interception, it is announcing a non-populated new object now. You can ask yourself if this is important (who writes interceptors which need the data of a newly created and non-saved object?), but it would be a lot better to separate these actions, so we have TWO actions, both with a single responsibility (process injections and announce interception).
So process the injections directly after creation (for example by calling the processEntityInjection
method of the orm event handler, which is a private method now ) and announce the interception at the end by calling a second method, so we still have an interception point which has the same data as before.
Leave a Reply