2

I'm using CFWheels to develope an application in ColdFusion.

I have a model called Vote.cfc. Before a Vote object can be created, updated, or deleted, I need to fetch a post object from another model: Post.cfc. A Vote belongs to a Post. A post has many votes.

Using data from the post object, I need to validate the vote object across multiple criteria and several functions. The only way I can think of persisting the post object so that it's available to those functions is by storing it in the request scope.

Others have said this is bad practice. But I was not able find out why. I thought the request scope was thread safe, and it would make sense to use it in this situation.

My other alternative is to load a new instance of the post object in every function that requires it. Although Wheels uses caching, doing this caused request times to spike by 250%.

UPDATE

Here's some samples. First, the controller handles looking to see if a vote object already exists. If it does, it deletes it, if it does not, it creates it. The controller function is essentially a toggle function.

Votes.cfc Controller

   private void function toggleVote(required numeric postId, required numeric userId)
    {
    // First, we look for any existing vote
    like = model("voteLike").findOneByPostIdAndUserId(values="#arguments.postId#,#arguments.userId#");

    // If no vote exists we create one
    if (! IsObject(like))
    {
        like = model("voteLike").new(userId=arguments.userId, postId=arguments.postId);
    }
    else
    {
        like.delete()
    }           
}

Model VoteLike.cfc

After that, a callback registered in the model fires before validation. It calls a function that retrieves the post object the vote will belong to. The function getPost() stores the post in the request scope. It is now made available to a bunch of validation functions in the model.

// Load post before Validation via callback in the Constructor
beforeValidation("getPost");

private function getPost()
{
    // this.postId will reference the post that the vote belongs to
    request.post = model("post").findByKey(this.postId);
}

// Validation function example
private void function validatesIsNotOwnVote()
{
    if (this.userId == request.post.userId)
    {
       // addError(message="You can't like your own post.");
    }
}

The alternative to the getPost() function is to use a scoped call "this.post().userId" to get the post object, as such:

private void function validatesIsNotOwnVote()
{
    if (this.userId == this.post().userId)
    {
        addError(message="can't vote on your own post")
    }
}

But I would then have to repeat this scoped call this.post().userId for EVERY function, which is what I think is slowing down the request!

Mohamad
  • 34,731
  • 32
  • 140
  • 219
  • @Mel: based on @Daniel Short's answer, and comments beneath, can you provide some clarification about how your code is structured? – orangepips Feb 02 '11 at 21:24
  • @orangepips, I updated my question with some sample code! – Mohamad Feb 02 '11 at 22:05
  • Is all validation done inside VoteLike.cfc? If so, then you can just set it to Variables.post = model("post").findByKey(this.postId), and then reference Variables.post instead of request.post. – Dan Short Feb 02 '11 at 22:08
  • @Daniel, actually no, my validation functions are in the base Vote.cfc model. VoteLike.cfc and VoteDislike.cfc use inheritance and extend Vote.cfc. Since they share the same validation, they reference the validation functions in Vote.cfc. Also, I thought the variables scope was not thread safe? – Mohamad Feb 02 '11 at 22:14
  • Just saw your other edit. Use the Variable scope to make a variable available to the entire CFC. – Dan Short Feb 02 '11 at 22:15
  • Regarding your inheritance, that sounds perfectly fine. Try variables.Post instead of Request or This.post. You may need (and I'm not 100% certain on this) to set Variables.Post in the pseudo-constructor to make sure it's available to the entire CFC. – Dan Short Feb 02 '11 at 22:16
  • @Daniel, I thought the variables scope was not thread safe... am I missing something? – Mohamad Feb 02 '11 at 22:25
  • 1
    The Variables scope inside of a CFC is as threadsafe as CFC properties. Meaning that if the CFC is not shared, and is modified within a single request, then there's no issue with using the Variables scope within the CFC itself. The variables scope inside of a CFC will not leak out of the CFC itself. – Dan Short Feb 02 '11 at 22:27
  • @Daniel, thanks for clarifying that. If I'm using inheritance, would the variables scope be only available to the base model, or also any models that extend it? So if I set variables.post in Vote.cfc, is it going to available in VoteLike.cfc which extends Vote.cfc – Mohamad Feb 02 '11 at 22:38
  • Yes, it will. The Variables scope is available from the top all the way to the bottom of an inheritance stack. – Dan Short Feb 02 '11 at 22:40
  • @Mel: assuming `VoteLike` is passed around and its `getPost()` method is accessible to other objects (i.e. `Vote`), @Dan Short's got the correct answer here in the comments. – orangepips Feb 03 '11 at 14:09
  • Considering revising my entire answer below based on the fact that Mel was already using composition to piece his objects together, and I assumed that wasn't the case when I wrote my answer. – Dan Short Feb 03 '11 at 15:44
  • All done. I left the original answer since it still has some good information in about how objects are passed byRef instead of byVal. – Dan Short Feb 03 '11 at 18:43

2 Answers2

2

Update with new answer based on comment thread in OP

Since you're extending the base Vote object form VoteLike.cfc, the CFCs share a local thread-safe variables scope (so long as you're not storing this in Application or Server scope where it can be fiddled with by others). This means that you set a value, such as Variables.Post, once, and reference that in any function within the stack of CFCs.

So change your getPost() function to this:

beforeValidation("getPost");

private function getPost(){
    Variables.Post = model("post").findByKey(this.postID);
}

Now, within any function in either VoteLike.cfc, you can reference Variables.Post. This means that you have a single instance of Post in the local CFCs variables scope, and you don't have to pass it around via arguments or other scopes.

Original Answer below

The "most correct" way to handle this would be to pass the object to each individual function as an argument. That, or add the Post as a property of the Vote object, so that the Vote object has access to the full Post object.

<cffunction name="doTheValidation">
    <cfargument name="ThePost" type="Post" required="true" />
    <cfargument name="TheVote" type="Vote" required="true" />
    <!--- do stuff here --->
</cffunction>

The reason this is a bad practice is because you're requiring that your objects have access to an external scope, that may or may not exist, in order to do their work. If you decided that you wanted to deal with multiple votes or posts on a single page, you then have to fiddle with the external scope to get things to work right.

You're far better off passing your object around, or using composition to piece your objects together in such a fashion that they're aware of each other.

Update

Regarding performance issues, if you use composition to tie your objects together, you end up with just two objects in memory, so you don't have to instantiate a bunch of unnecessary Post objects. Also, passing a CFC into a function as an argument will pass the CFC by reference, which means that it's not creating a duplicate of the CFC in memory, which should also take care of your performance concerns.

Code Sample Update

To illustrate the "by reference" comment above, set up the following files and put them in a single directory on their own.

TestObject.cfc:

<cfcomponent output="false">
    <cfproperty name="FirstName" displayname="First Name" type="string" />
    <cfproperty name="LastName" displayname="Last Name" type="string" />

</cfcomponent>

index.cfm:

<!--- Get an instance of the Object --->
<cfset MyObject = CreateObject("component", "TestObject") />

<!--- Set the initial object properties --->
<cfset MyObject.FirstName = "Dan" />
<cfset MyObject.LastName = "Short" />

<!--- Dump out the object properties before we run the function --->
<cfdump var="#MyObject#" label="Object before passing to function" />

<!--- Run a function, sending the object in as an argument, and change the object properties --->
<cfset ChangeName(MyObject) />

<!--- Dump out the object properites again, after we ran the function --->
<cfdump var="#MyObject#" label="Object after passing to function" />

<!--- Take a TestObject, and set the first name to Daniel --->
<cffunction name="ChangeName">
    <cfargument name="TheObject" type="TestObject" required="true" hint="" />
    <cfset Arguments.TheObject.FirstName = "Daniel" />
</cffunction>

You'll notice when you run index.cfm, that the first dump has the FirstName as Dan, while the second has the FirstName as Daniel. That's because CFCs are passed to functions by reference, which means that any changes made within that function are made to the original object in memory. Hence no recreation of objects, so no performance hit.

Dan

Community
  • 1
  • 1
Dan Short
  • 9,598
  • 2
  • 28
  • 53
  • How does this address the performance issue raised in the question? – orangepips Feb 02 '11 at 19:00
  • Because you're not creating additional instances of the objects, as the original poster stated as his workaround for not using the request scope. You're either using composition, and still only dealing with the two objects, or you're passing objects into functions, which is still not instantiating a new object. – Dan Short Feb 02 '11 at 20:14
  • Short: My read of the question is multiple templates/CFCs within the request call some retrieval method (i.e `getPost()`). Unless I'm missing something, this answer's approach requires refactoring client code to pass along this Post object - likely the framework has some wrapper for this purpose - instead of the multiple calls to `getPost()`. So yes, instantiating once is a good idea, but a large refactoring not so much. – orangepips Feb 02 '11 at 21:23
  • Agreed, there will have to be some code refactoring, since it's not working well already :), hopefully (given the new code sample), it's as easy as just refactoring within VokeLike.cfc. – Dan Short Feb 02 '11 at 22:08
  • Short and @orangepips, I just added an example of doing a scoped call to the post model at the bottom of my examples. The process which makes the page requests long. – Mohamad Feb 02 '11 at 22:15
1

request is a global scope, confined to the page request (i.e. thread safe), that lives for the duration of that page request. So it's bad practice in the same way that global variables are a bad practice, but of a short lived duration. I think of it as throwing the data up in the air or over the fence in the situation you described, where any other code can just pluck out of mid air.

So for your situation it's probably just fine - add some useful reference at the point of consumption perhaps about where the data's being placed into the request scope in the first place. That stated, if you are going back to the same source method each time , consider caching inside whatever function is responsible creating that object therein (e.g. getVote()) And, you could use the request scope for that as such:

<cfparam name="request.voteCache" default="#structNew()#"/>
<cffunction name="getVote" output="false" access="public" returntype="any">
    <cfargument name="voteId" type="string" required="true"/>
    <cfif structKeyExists(request.voteCache, arguments.voteId)>
        <cfreturn request.voteCache[arguments.voteId]>
    </cfif>

    <!--- otherwise get the vote object --->
    <cfset request.voteCache[arguments.voteId] = vote>
    <cfreturn vote>
</cffunction>

Downside is if something else changes the data during the request you'll have a stale cache, but it appears you don't expect any changes during the execution.

orangepips
  • 9,891
  • 6
  • 33
  • 57