It's been a while since I blogged about Transfer, but I finally got time to look into caching and performance issues with Transfer. I'm pretty impressed by what I've found. If I had known about some of this back when I was working on CFLib 2008, I probably would have done things quite a bit differently. (In fact, I may take a look at re-building things a bit and blogging about the changes.) Here is a quick summary of some of the things you can do to improve performance issues with Transfer.
First, let me quickly address caching in Transfer. Out of the box, Transfer is pretty smart. Imagine this scenario based on the Employee Directory sample I've used for my other entries. You have some employee that has a benefit. The benefit has an id of 5. If you fetch benefits later and grab the benefit with the primary key of 5, Transfer knows that it loaded it already when it fetched the employee. It will grab the data from it's own cache instead of requerying the database. Transfer will, by default, cache within itself. By that I mean it will cache everything within it's own factory object. This is called the "instance" caching. You have multiple other options for how Transfer will store it's cache:- instance: Stores the data in the Transfer object. This is the default.
- application: Stores data in the application scope.
- session: Stores data in the session scope.
- transaction: Stores the data in the session scope, but notices changes made to objects across the board and will clear the object from the cache.
- request: Stores data in the request scope.
- server: Stores data in the server scope. This is useful for sharing a cache amongst multiple applications. I had a reader ping me about this just this past week.
- none: No caching. Were you able to guess that?
2 <defaultcache>
3 <scope type="none" />
4 </defaultcache>
5 </objectCache>
2 <defaultcache>
3 <scope type="none" />
4 </defaultcache>
5 <cache class="employee">
6 <scope type="session" />
7 </cache>
8 </objectCache>
2 <link to="employee" column="employeeidfk"/>
3 <link to="benefit" column="benefitidfk"/>
4 <collection type="array">
5 <order property="name" order="asc"/>
6 </collection>
7 </manytomany>
With lazy=true, the object is now slimmer:
What's awesome though is that as soon as you need the data, Transfer has no trouble getting it. Consider:
2 <cfdump var="#emp.getMemento()#" label="Employee Memento">
3
4 <cfif structKeyExists(url, "showbenefits")>
5 <cfdump var="#emp.getBenefitsArray()#" label="Benefits">
6 </cfif>
7 <cfabort/>
2 <link to="position" column="employeeidfk" />
3 <collection type="array">
4 <order property="startdate" order="desc" />
5 </collection>
6 </onetomany>
2 <cfdump var="#emp.getMemento()#" label="Employee Memento">
3
4 <cfset positions = emp.getPositionsArray()>
5 <cfoutput>Total positions: #arrayLen(positions)#<br/></cfoutput>
6 <cfloop index="x" from="1" to="#arrayLen(positions)#">
7 <cfif x is 1>
8 <cfoutput>Current Job: #positions[x].getName()#<br/></cfoutput>
9 </cfif>
10 <cfoutput>position id = #positions[x].getID()#, isLoaded? #positions[x].getIsLoaded()#<br/></cfoutput>
11 </cfloop>
Notice how the first object is reported as loaded while the second is not. Again, this is exactly the problem I ran into at CFLib.
Comment 1 written by Tim Garver on 27 December 2008, at 4:19 PM
Another great article!
This really helps me understand it much better than the docs.
I am wondering about stability though. In my own application I am writing, for some reason transfer seems to reload itself quite often. I have my cache setup under the Server scope and share it among 3 applications on the box.
I was also wondering about hundreds of transactions at the same time and how or if transfer uses some sort of locking mechanism? How does it handle multiple transactions on the same record?
Thanks again
Tim
Comment 2 written by Raymond Camden on 30 December 2008, at 9:38 AM
Comment 3 written by John Whish on 30 December 2008, at 2:30 PM
Comment 4 written by Sean Corfield on 11 January 2009, at 4:53 PM
The other big gotcha is the cross-scope issue that Ray alluded to (indeterminate behavior). What tends to happen if you have related objects cached in different scopes is that one end of the relationship expires and may be reloaded but the other end of the relationship still points to the "old" instance in memory. At that point, changes to the object will either update the old in-memory instance or the new in-cache instance depending on how you reference the object.
That's why you should never put a Transfer Object in session scope (or application scope). Put the PK in a shared scope by all means, but always get the object by fetching it via Transfer (if the object is in cache, it won't hit the database anyway).
As to the memory leak, Mark Mandel, Mike Brunt and I have been trying to track that down for a long time. We usually can't reproduce it on a test server, even under load, but we see it on our production servers. And, yes, we've tried all the obvious approaches and several non-obvious ones and we've even reconfigured a number of things that seemed to be unique to production in an effort to eliminate every possibility. It's quite intriguing (and rather annoying). It's also not clear whether Transfer is the culprit, to be honest. If we ever figure it out, we'll blog about it :)
Comment 5 written by Josh Nathanson on 4 March 2009, at 5:59 PM
What's really frustrating is my coworker has the exact same codebase and data on his sandbox and he is not seeing the same issue. Meanwhile my stack blows up and I get "500 out of memory" errors when I try to retrieve 50 transfer objects.
Sounds a lot like the issue you guys have been seeing. We are continuing to troubleshoot so if we find anything out I'll post back over here.