Determining if an entity is within a transaction

13 views
Skip to first unread message

Tristan Lee

unread,
Jan 30, 2015, 12:31:45 PM1/30/15
to cf-or...@googlegroups.com
I am trying to put together a AuditableEntity that gets the initial memento of the entity when it was loaded (via postLoad()), and gets it again when the entity was updated (via postUpdate()) so I can compare changes. That functionality is working fine, but unfortunately, postLoad() is executed for all entities that are loaded which can be expensive since the majority of the time I don't care about their initial memento since they're only being loaded to reference data.

That being said, the pattern being followed is anytime I intend to persist an entity, it's wrapped within a transaction from load to save. Here is my test:

transaction {
e1 = entityLoadByPK("Center", "2a5996c64b0775ca014b31617acb0163");
writedump(e1.getEntityHash());
}

transaction {
e2 = entityLoadByPK("Center", "50A739D3BE114CF9AEA93C9FC1724EF8");
writedump(e2.getEntityHash());
}


Both of these entities are valid and successfully loaded. The postLoad() event is triggered, which is:

public void function postLoad () {
// if the load is within a transaction, we're assuming
// it has intent to be persisted, so copy to memento
if (ormGetSession().isTransactionInProgress()) {
initialMemento = copyMemento();
}
}

The as you can see, post loads with within their own transaction. However, in the postLoad() handler, isTransactionInProgress() only return TRUE for the 2nd transaction, not the first. Either my understanding of the transaction is not complete, or there is an underlying issue on how a transaction is determined to be in progress.

Any thoughts on how, within postLoad(), I can determine if the entity is currently wrapped within a transaction?

Tristan Lee

unread,
Jan 30, 2015, 12:44:17 PM1/30/15
to cf-or...@googlegroups.com
It turns out this is happening because in my first entity (2a5996c64b0775ca014b31617acb0163), that entity is actually already loaded once in onRequestStart(), therefore it's already in the secondary cache without change and Hibernate knows that it had not changed between the initial load and the currently load in my test script. Therefore, I can only assume that's why it's returning that no transaction is currently active...

Brian Kotek

unread,
Feb 3, 2015, 10:17:16 AM2/3/15
to cf-or...@googlegroups.com
It's been a long time since I did anything like this with CF, but in Java/Groovy, I'd create an empty Auditable interface and have the domain objects that you want to audit implement that interface. Then, in your audit interceptor, you can just check whether the object being changed implements Auditable. I *think* this would work in CF as well, but as I said, I'm not completely sure.


--
You received this message because you are subscribed to the Google Groups "cf-orm-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cf-orm-dev+...@googlegroups.com.
To post to this group, send email to cf-or...@googlegroups.com.
Visit this group at http://groups.google.com/group/cf-orm-dev.
For more options, visit https://groups.google.com/d/optout.

Brian Kotek

unread,
Feb 3, 2015, 10:19:04 AM2/3/15
to cf-or...@googlegroups.com
Hmm, just to add, I wouldn't be trying to do this in postLoad(). Shouldn't you be using postInsert()/postUpdate()?

Tristan Lee

unread,
Feb 3, 2015, 11:53:49 AM2/3/15
to cf-or...@googlegroups.com
On Tuesday, February 3, 2015 at 10:19:04 AM UTC-5, Brian Kotek wrote:
> Hmm, just to add, I wouldn't be trying to do this in postLoad(). Shouldn't you be using postInsert()/postUpdate()?

Thanks for the feedback, Brian. I am following a similar pattern you mentioned.

Department.cfc
`component persistent="true" table="vw_hier_departments" extends="models.AuditableEntity" implements="models.IAuditable,models.IValidatable"`

There are actions happening on postLoad() and postInsert()/postUpdate(). On postLoad(), I am getting the current state of the entity. When postInsert()/postUpdate() are invoked, I then get that state of the entity. I then compare the changes between the mementos and then log them accordingly. One thing I added to AuditableEntity.cfc is a non-persistent property `intentToPersist` which is set manually in the code. Its purpose is to flag that I have loaded this eneity and I intend to modify it and persist changes. The reason for this is I do not want postLoad() to always make a copy of the memento each time an entity is loaded. That creates a lot of overhead on entities I don't plan on persisting. So what I typically do is this:

var oCenter = entityLoadByPK('Center', centerID);

// entity did not exist already
if (!structKeyExists(local, 'oCenter')) {
oCenter = entityNew('Center');
}

oCenter.setIntentToPersist(true);


AuditableEntity.cfc
-------------------
component mappedsuperclass="true" persistent="false" extends="models.BaseEntity" accessors="true" {
property name="intentToPersist" persistent="false" type="boolean" default="false";

variables.initialMemento = {};
variables.modifiedMemento = {};

/**
* Force the subclass to implement the proper interface
*/
public any function init () {
if (!isInstanceOf(this, "models.IAuditable")) {
throw (message="Entity #ucase(getEntityname())# must implement the following interface: models.IAuditable");
}

var _this = super.init();

return _this;
}

/**
* When set to <tt>true</tt>, this will invoke postLoad() (without reloading the entity)
* @output false
* @return void
*/
public void function setIntentToPersist (required boolean doPersist) {
intentToPersist = doPersist;

if (doPersist) {
postLoad();
}
}

public void function postLoad () {
if (ormGetSession().isTransactionInProgress() || getIntentToPersist()) {
initialMemento = copyMemento();
}
}

public void function postUpdate() {
modifiedMemento = copyMemento();

var changedData = getChanges(initialMemento, modifiedMemento);
var data = {memento = cleanMemento(modifiedMemento), changedData = changedData};
var entityData = {
id = getIdentityValue(),
table = getTableName(),
isDeleted = arrayFindNoCase(getPersistedProperties(), "isDeleted") && getIsDeleted()
};

// HACK: for whatever reason, this entity will not persist in the current session
// so putting it in a thread and watching for exceptions is the workaround
thread name="tAuditLogUpdate#createUUID()#" action="run" entity=local.entityData rowData=local.data {
transaction {
oAuditLog = entityNew('AuditLog');
oAuditLog.setRowID(attributes.entity.id);
oAuditLog.setTableName(attributes.entity.table);
oAuditLog.setRowData(attributes.rowData.memento);

if (attributes.entity.isDeleted) {
// Record has been soft deleted
oAuditLog.setChangeType('softdelete');
} else {
// Normal Update
oAuditLog.setChangeType('update');
}

if (structCount(attributes.rowData.changedData) > 0) {
oAuditLog.setChangeNotesFromStruct(attributes.rowData.changedData);
}

entitySave(oAuditLog);
}
}
thread action="join";

checkThreads(cfthread);
}

...
}

Brian Kotek

unread,
Feb 3, 2015, 1:02:58 PM2/3/15
to cf-or...@googlegroups.com
I'm still thinking you shouldn't have to do anything with postLoad() here. preUpdate() supplies the old state of the entity to the event handler...wouldn't that be sufficient?

As a side note, the limitation on making changes while the session is being flushed is intentional. Otherwise, you could change the set of entities marked for persisting after Hibernate has already determined what it's going to persist. This is why you have to do it in a new transaction (and, thus, a new session).

}

Tristan Lee

unread,
Feb 3, 2015, 1:12:00 PM2/3/15
to cf-or...@googlegroups.com
Originally, using the changed values supplied to preUpdate() did work until the scope changed and required more information about the entity logged, such as relationship IDs that potentially changed in the many-to-many properties.

As you noted regarding my "HACK" comment in the code, it's strange that Railo and ACF handle that different. In Railo, I was able to create a new transaction within the same session to log the audit details. ACF would throw an exception either that no session existed or some error regarding flush(). Putting this in a thread satisfied both platforms.
Reply all
Reply to author
Forward
0 new messages