Macromedia Coldfusion MX 7 Web Application Construction Kit
The ColdFusion components discussed so far in this chapter (the FilmSearchCFC and FilmDataCFC examples) have both been static components, meaning they don't hold any instance data. That is, although you can create an instance of a component with <cfobject> before using it, there really isn't any need to do so. One instance of a component isn't going to behave any differently from any other instance, so it's fine to simply call the CFC's methods directly. If you create components that hold instance data, though, each instance of the component lives on its own and has its own memory in which to store information. If your component is about films, each instance might be an individual film and the instance data might be the film's title, budget, gross receipts, or even critics' reviews. If your component is about shopping carts, each instance of the component would represent a separate user's shopping cart, and the instance data would be the cart's contents. This section will explain how to create this type of component. Introducing the THIS Scope
The CFC framework sets aside a special variable scope called THIS, which stands for this instance of a component. You can think of the word THIS as meaning "this film" or "this shopping cart" or "this object," depending on what you intend your component to represent. The THIS Scope Represents an Instance
The THIS scope is similar in its function to the SESSION scope you learned about in Chapter 20, except that instead of being a place to store information that will be remembered for the duration of a user's session, THIS is a place to store information that will be remembered for as long as a particular instance of a component continues to exist. As an example, consider a fictional CFC called ParrotCFC. The idea behind the component is that each instance of the component represents one parrot. Each instance of the component needs to have a name, an age, a gender, a wingspan, a favorite word or cracker, and so on. This kind of information is exactly what the THIS scope was designed for. Your CFC code just needs to set variables in the THIS scope (perhaps THIS.favoriteWord or THIS.wingSpan) to remember these values. ColdFusion will keep each component's variables separate. Steps in the THIS Process
Here are the steps involved:
In this scenario, each individual instance of the ParrotCFC has a life of its own. The <cfobject> tag is what makes a particular parrot come to life. The THIS scope automatically maintains the parrot's characteristics. Extending the metaphor, if the parrot is the pet of one of your Web users, you can make the parrot follow the user around by having it live in the user's SESSION scope. Or if the parrot doesn't belong to a particular person but instead belongs to your application as a whole (perhaps the parrot is your site's mascot), you could have it live in the APPLICATION scope. Or you might have a bunch of parrots that are looking for owners. You could keep these parrots (each one an instance of the ParrotCFC component) in an array in the APPLICATION scope. When a user wants to take one of the parrots home as a pet, you could move the parrot out of the array and into the SESSION scope. OK, that's enough about parrots. The idea here is to think of a CFC as an independent thing or object with its own properties. You store individual instances of the object in the APPLICATION or SESSION scope if you want it to remain in memory for a period of time, or just leave it in the normal scope if you only need the instance to live for the current page request. NOTE By definition, a component that doesn't refer to the THIS scope at all in its methods doesn't need to be instantiated with <cfobject> before calling its methods, and can therefore be considered a static component. Any component that does use the THIS scope internally probably needs to be instantiated to function properly.
An Instance Data CFC Example
Let's look at a simple example of a CFC that holds instance data. The component is called FilmRotationCFC, and its purpose is to keep track of a featured film. Designing FilmRotationCFC
To demonstrate the use of multiple methods within an instantiated component, the FilmRotationCFC component will contain five methods, as listed in Table 23.10.
TIP In this CFC, I am adopting a convention of starting all public method names with the word Get. You might want to consider using naming conventions such as this when creating your own component methods.
TIP It is conventional in many programming languages to start the name of any function that returns a Boolean value with the word Is. You might want to consider doing the same in your own CFCs.
Building FilmRotationCFC
Listing 23.26 shows the code for the FilmRotationCFC component. Because this component includes a number of methods, this code listing is a bit long. Don't worry. The code for each of the individual methods is quite short. Listing 23.26. FilmRotationCFC.cfcBuilding a CFC That Maintains Instance Data
<!--- Filename: FilmRotationCFC.cfc Author: Nate Weiss (NMW) Purpose: Creates FilmRotationCFC, a ColdFusion Component ---> <cfcomponent output="false"> <!--- *** begin initialization code *** ---> <cfset THIS.filmList = randomizedFilmList()> <cfset THIS.currentListPos = 1> <cfset THIS.rotationInterval = 5> <cfset THIS.currentUntil = dateAdd("s", THIS.rotationInterval, now())> <!--- *** end initialization code *** ---> <!--- Private function: RandomizedFilmList() ---> <cffunction name="randomizedFilmList" returnType="string" access="private" output="false" hint="For internal use. Returns a list of all Film IDs, in random order."> <!--- This variable is for this function's use only ---> <cfset var getFilmIDs = ""> <!--- Retrieve list of current films from database ---> <cfquery name="getFilmIDs" datasource="ows" cachedwithin="#CreateTimeSpan(0,1,0,0)#"> SELECT FilmID FROM Films ORDER BY MovieTitle </cfquery> <!--- Return the list of films, in random order ---> <cfreturn listRandomize(valueList(getFilmIDs.FilmID))> </cffunction> <!--- Private utility function: ListRandomize() ---> <cffunction name="listRandomize" returnType="string" output="false" hint="Randomizes the order of the items in any comma-separated list."> <!--- List argument ---> <cfargument name="list" type="string" required="Yes" hint="The string that you want to randomize."> <!--- These variables are for this function's use only ---> <cfset var result = ""> <cfset var randPos = ""> <!--- While there are items left in the original list... ---> <cfloop condition="listLen(ARGUMENTS.list) gt 0"> <!--- Select a list position at random ---> <cfset randPos = randRange(1, listLen(ARGUMENTS.list))> <!--- Add the item at the selected position to the Result list ---> <cfset result = listAppend(result, listGetAt(ARGUMENTS.list, randPos))> <!--- Remove the item from selected position of the original list ---> <cfset ARGUMENTS.list = listDeleteAt(ARGUMENTS.list, randPos)> </cfloop> <!--- Return the reordered list ---> <cfreturn result> </cffunction> <!--- Private method: IsFilmNeedingRotation() ---> <cffunction name="isFilmNeedingRotation" access="private" returnType="boolean" output="false" hint="For internal use. Returns TRUE if the film should be rotated now."> <!--- Compare the current time to the THIS.CurrentUntil time ---> <!--- If the film is still current, DateCompare() will return 1 ---> <cfset var dateComparison = dateCompare(THIS.currentUntil, now())> <!--- Return TRUE if the film is still current, FALSE otherwise ---> <cfreturn dateComparison neq 1> </cffunction> <!--- RotateFilm() method ---> <cffunction name="rotateFilm" access="private" returnType="void" output="false" hint="For internal use. Advances the current movie."> <!--- If the film needs to be rotated at this time... ---> <cfif isFilmNeedingRotation()> <!--- Advance the instance-level THIS.CurrentListPos value by one ---> <cfset THIS.currentListPos = THIS.currentListPos + 1> <!--- If THIS.CurrentListPos is now more than the number of films, ---> <!--- Start over again at the beginning (the first film) ---> <cfif THIS.currentListPos gt listLen(THIS.FilmList)> <cfset THIS.currentListPos = 1> </cfif> <!--- Set the time that the next rotation will be due ---> <cfset THIS.currentUntil = dateAdd("s", THIS.rotationInterval, now())> </cfif> </cffunction> <!--- Private method: CurrentFilmID() ---> <cffunction name="currentFilmID" access="private" returnType="numeric" output="false" hint="For internal use. Returns the ID of the current film in rotation."> <!--- Return the FilmID from the current row of the GetFilmIDs query ---> <cfreturn listGetAt(THIS.filmList, THIS.currentListPos)> </cffunction> <!--- Public method: GetCurrentFilmID() ---> <cffunction name="getCurrentFilmID" access="public" returnType="numeric" output="false" hint="Returns the ID number of the currently 'featured' film."> <!--- First, rotate the current film ---> <cfset rotateFilm()> <!--- Return the ID of the current film ---> <cfreturn currentFilmID()> </cffunction> <!--- Public method: GetCurrentFilmData() ---> <cffunction name="getCurrentFilmData" access="remote" returnType="struct" output="false" hint="Returns structured data about the currently 'featured' film."> <!--- This variable is local just to this function ---> <cfset var currentFilmData = ""> <!--- Invoke the GetCurrentFilmID() method (in separate component) ---> <!--- Returns a structure with film's title, summary, actors, etc. ---> <cfinvoke component="FilmDataCFC2" method="getFilmData" film returnVariable="currentFilmData"> <!--- Return the structure ---> <cfreturn currentFilmData> </cffunction> </cfcomponent>
The most important thing to note and understand about this CFC is the purpose of the first few <cfset> tags at the top of Listing 23.26. Because these lines sit directly within the body of the <cfcomponent> tag, outside any <cffunction> blocks, they are considered initialization code that will be executed whenever a new instance of the component is created. Notice that each of these <cfset> tags creates variables in the special THIS scope, which means they are assigned to each instance of the component separately. Typically, all that happens in a CFC's initialization code is that it sets instance data in the THIS scope. NOTE It's important to understand that these lines don't execute each time one of the instance's methods is called. They execute only when a new instance of the component is brought to life with the <cfobject> tag.
The <cfset> tags at the top of the listing create these instance variables:
Let's take a quick look at the <cffunction> blocks in Listing 23.26. The randomizedFilmList() method will always be the first one to be called, since it is used in the initialization code block. This method simply retrieves a record set of FilmIDs from the database. Then it turns the FilmIDs into a comma-separated list with ColdFusion's valueList() function and passes the list to the CFC's listRandomize() method. The resulting list (which is a list of films in random order) is returned as the method's return value. The listRandomize() method uses a combination of ColdFusion's list functions to randomize the list supplied to the list argument. The basic idea is to pluck items at random from the original list, adding them to the end of a new list called result. When there are no more items in the original list, the result variable is returned as the method's return value. See Appendix C for details on the list functions used here. The currentFilmID() method simply returns the FilmID in the current position of the CFC's randomized list of films. As long as THIS.currentListPos is set to 1, this method returns the first film's ID. The isFilmNeedingRotation() method uses dateCompare() to compare THIS.currentUntil to the current time. If the time has passed, this method returns TRUE to indicate that the current film is ready for rotation. The rotateFilm() method is interesting because it actually makes changes to the variables in the THIS scope first created in the initialization code block. First, it uses isFilmNeedingRotation() to see whether the current film has been featured for more than 5 seconds already. If so, it advances the This.currentListPos value by 1. If the new currentListPos value is greater than the length of the list of films, that means all films in the sequence have been featured, so the position is set back to 1. Lastly, the method uses ColdFusion's dateAdd() function to set the THIS.currentUntil variable to 5 seconds in the future. The getCurrentFilmID() method ties all the concepts together. Whenever this method is used, the rotateFilm() method is called (which will advance the current film to the next item in the sequence if the current one has expired). It then calls currentFilmID() to return the current film's ID. Storing CFCs in the APPLICATION Scope
Now that the FilmRotationCFC component is in place, it's quite simple to put it to use. Listing 23.27 shows one way of using the component. Listing 23.27. UsingFilmRotationCFCa.cfmInstantiating a CFC at the Application Level
<!--- Filename: UsingFilmRotationCFCa.cfm Author: Nate Weiss (NMW) Purpose: Demonstrates storage of CFC instances in shared memory scopes ---> <html> <head> <title>Using FilmRotationCFC</title> </head> <body> <!--- If an instance of the FilmRotatorCFC component hasn't been created ---> <!--- yet, create a fresh instance and store it in the APPLICATION scope ---> <cfif not isDefined("APPLICATION.filmRotator")> <cfobject component="FilmRotationCFC" name="APPLICATION.FilmRotator"> </cfif> <!--- Invoke the GetCurrentFilmID() method of the FilmRotator CFC object ---> <cfinvoke component="#APPLICATION.filmRotator#" method="getCurrentFilmID" returnVariable="featuredFilmID"> <p>The callout at the right side of this page shows the currently featured film. The featured film changes every five seconds. Just reload the page to see the next film in the sequence. The sequence will not change until the ColdFusion server is restarted.</p> <!--- Show the current film in a callout, via custom tag ---> <cf_ShowMovieCallout film> </body> </html>
The idea here is to keep an instance of FilmRotationCFC in the APPLICATION.filmRotator variable. Keeping it in the APPLICATION scope means that the same instance will be kept in the server's memory until the ColdFusion server is restarted. All sessions that visit the page will share the instance. First, a simple isDefined() test sees if the CFC instance called APPLICATION.filmRotator already exists. If not, the instance is created with the <cfobject> tag. So, after this <cfif> block, the instance is guaranteed to exist. Keep in mind that the CFC's initialization code block is executed when the instance is first created (refer to Listing 23.25). NOTE If you wanted the CFC instance to be available to all pages in the application, you could move the <cfif> block in Listing 23.27 to your Application.cfc file.
Displaying the currently featured film is simply a matter of calling the getCurrentFilmID() method with the <cfinvoke> tag and passing it to the <cf_ShowMovieCallout> custom tag created near the beginning of this chapter. When a browser visits this listing, the currently featured movie is displayed (Figure 23.13). If you reload the page repeatedly, you will see that the featured movie changes every 5 seconds. If you wait long enough, you will see the sequence of films repeat itself. The sequence will continue to repeat until the ColdFusion server is restarted, at which point a new sequence of films will be selected at random. Figure 23.13. CFCs can be stored in shared variable scopes to provide interesting, controlled user experiences.
It's worth noting that you can use the <cfscript> syntax to call methods of CFC instances in the APPLICATION scope. Depending on the situation, the <cfscript> syntax may be more clear. Listing 23.28 does the same thing as Listing 23.27, except that it replaces the <cfinvoke> tag with the <cfscript> syntax. This shortens the whole listing considerably. Listing 23.28. UsingFilmRotationCFCb.cfmUsing <cfscript> Syntax with Persisted CFCs
<!--- Filename: UsingFilmRotationCFCb.cfm Author: Nate Weiss (NMW) Purpose: Demonstrates storage of CFC instances in shared memory scopes ---> <html> <head> <title>Using FilmRotationCFC</title> </head> <body> <!--- If an instance of the FilmRotatorCFC component hasn't been created ---> <!--- yet, create a fresh instance and store it in the APPLICATION scope ---> <cfif not isDefined("APPLICATION.filmRotator")> <cfobject component="filmRotationCFC" name="APPLICATION.filmRotator"> </cfif> <p>The callout at the right side of this page shows the currently featured film. The featured film changes every five seconds. Just reload the page to see the next film in the sequence. The sequence will not change until the ColdFusion server is restarted.</p> <!--- Show the current film in a callout, via custom tag ---> <cf_ShowMovieCallout film> </body> </html>
Storing CFCs in the SESSION Scope
One of the neat things about CFCs is their independence. You will note that the code for the RotateFilmCFC component doesn't contain a single reference to the APPLICATION scope. In fact, it doesn't refer to any of ColdFusion's built-in scopes at all, except for the THIS scope. This means it's possible to create some instances of the CFC that are kept in the APPLICATION scope, and others that are kept in the SESSION scope. All the instances will work properly, and will maintain their own versions of the variables in the THIS scope. To see this in action, go back to either Listing 23.27 or Listing 23.28 and change the code so that the CFC instance is kept in the SESSION scope instead of the APPLICATION scope. Now each Web session will be given its own FilmRotator object, stored as a session variable. You can see how this looks in Listing 23.29 (in the next section, "Modifying Properties from a ColdFusion Page"). To see the difference in behavior, open up the revised listing in two different browsers (say, Netscape 6 and Internet Explorer 6), and experiment with reloading the page. You will find that the films are featured on independent cycles, and that each session sees the films in a different order. If you view the page on different computers, you will see that each machine also has its own private, randomized sequence of featured films. Instance Data as Properties
As I've explained, the code for the FilmRotationCFC component uses the THIS scope to store certain variables for its own use. You can think of these variables as properties of each component instance, because they are the items that make a particular instance special, that give it its individuality, its life. Sometimes you will want to display or change the value of one of these properties from a normal ColdFusion page. ColdFusion makes it very easy to access an instance's properties. Basically, you can access any variable in a CFC's THIS scope as a property of the instance itself. Modifying Properties from a ColdFusion Page
If you have a CFC instance called SESSION.myFilmRotator and you want to display the current value of the currentUntil property (that is, the value of the variable that is called THIS.currentUntil within the CFC code), you can do so with the following in a normal .cfm page: <cfoutput> #timeFormat(SESSION.myFilmRotator.currentUntil)# </cfoutput> To change the value of the rotationInterval property (referred to as THIS.rotationInterval in the FilmRotationCFC.cfc file) to 10 seconds instead of the usual 5 seconds, you could use this line: <cfset SESSION.myFilmRotator.rotationInterval = 10> After you changed the rotationInterval for the SESSION.FilmRotator instance, then that session's films would rotate every 10 seconds instead of every 5 seconds. Listing 23.29 shows how all this would look in a ColdFusion page. Listing 23.29. UsingFilmRotationCFCc.cfmInteracting with a CFC's Properties
<!--- Filename: UsingFilmRotationCFCc.cfm Author: Nate Weiss (NMW) Purpose: Demonstrates storage of CFC instances in shared memory scopes ---> <html> <head> <title>Using FilmRotationCFC</title> </head> <body> <!--- If an instance of the FilmRotatorCFC component hasn't been created ---> <!--- yet, create a fresh instance and store it in the SESSION scope ---> <cfif not isDefined("SESSION.myFilmRotator")> <cfobject component="FilmRotationCFC" name="SESSION.myFilmRotator"> <!--- Rotate films every ten seconds ---> <cfset SESSION.myFilmRotator.rotationInterval = 10> </cfif> <!--- Display message ---> <cfoutput> <p> The callout at the right side of this page shows the currently featured film. Featured films rotate every #SESSION.myFilmRotator.rotationInterval# seconds. Just reload the page to see the next film in the sequence. The sequence will not change until the web session ends.</p> The next film rotation will occur at: #timeFormat(SESSION.myFilmRotator.currentUntil, "h:mm:ss tt")# </cfoutput> <!--- Show the current film in a callout, via custom tag ---> <cf_ShowMovieCallout film> </body> </html>
NOTE You can experiment with changing the RotationInterval property to different values. Keep in mind that the code in the <cfif> block will only execute once per session, so you may need to restart ColdFusion to see a change. If you are using J2EE Session Variables, you can just close and reopen your browser. Or you could move the <cfset> line outside the <cfif> block. What all this means is that the CFC's methods can access an instantiated CFC's properties internally via the THIS scope, and your ColdFusion pages can access them via the instance object variable itself. As you learned in the introduction to this topic, CFCs can be thought of as containers for data and functionality, like many objects in the real world. You know how to access the data (properties) as well as the functionality (methods). CFCs and the VARIABLES Scope
The THIS scope isn't the only way to persist data within a CFC. Each CFC also has a VARIABLES scope. This scope acts just like the VARIABLES scope within a simple CFM page. Like the THIS scope, each method in the CFC can read and write to the scope. However, unlike the THIS scope, you can't display or modify the value outside of the CFC. Some people consider this a good thing. Look at the code in Listing 23.29. One line uses timeFormat() function to display the CFC's currentUntil variable. Now imagine that many other CFM pages on your site do the same thing. What happens if the writer of the CFC changes the name of that variable? All of sudden you have a bunch of errors on your site, since these documents were using the internal currentUntil variable of the CFC. The whole point of encapsulation is to prevent problems like this. How can we prevent this? I would suggest doing two things. Whenever you need to access a value from within a CFC, build a method. So for example, instead of directly accessing the currentUntil value of the CFC, the CFC itself could define a getCurrentUntil() method. Any CFM that needs this value would simply use the method. If the CFC changed the internal value, it wouldn't break anything, since the other documents would be using the method instead of directly accessing the value. Along with using methods to return instance data, consider using the VARIABLES scope instead of the THIS scope. This will actually prevent CFMs from reading the data, forcing you to use methods to access the data itself. Documenting Properties With <cfproperty>
As you learned earlier, you can easily view a CFC's methods in the Component tree in the Dreamweaver's Application panel. You can also view them in the automatically generated reference page that ColdFusion produces if you visit a CFC's URL with your browser. Since a CFC's properties are also important, it would be nice if there was an easy way to view them too. ColdFusion provides a tag called <cfproperty> that lets you provide information about each variable in the this scope that you want to document as an official property of a component. The <cfproperty> tags must be placed at the top of the CFC file, just within the <cfcomponent> tag, before any initialization code. The syntax for the <cfproperty> tag is shown in Table 23.11.
NOTE The <cfproperty> tag doesn't actively create a property in this version of ColdFusion. Adding the tag to a .cfc file doesn't cause your component to behave differently. It simply causes the information you provide in the <cfproperty> tag to be included in the automatic documentation and in the Dreamweaver MX interface.
NOTE If you're using the CFC framework to create Web Services, the <cfproperty> tag becomes important, because the property will become part of the published description of the service. Creating Web Services isn't much harder than creating ordinary CFCs, but it's beyond the scope of this book. For details, see our companion volume, "Advanced Macromedia ColdFusion MX 7 Application Development."
To document the rotationInterval property officially, you could add the following <cfproperty> tag to Listing 23.26, between the opening <cfcomponent> tag and the series of <cfset> tags: <!--- Property: Rotation Interval ---> <cfproperty name="RotationInterval" type="numeric" required="No" default="5" hint="The number of seconds between film rotations.">
NOTE Remember that the <cfproperty> doesn't actively create a property in this version of ColdFusion. Just because you add the <cfproperty> tag to document the THIS.rotationInterval property, it doesn't mean that you can remove the <cfset> tag that actually creates the variable and gives it its initial value.
CFCs, Shared Scopes, and Locking
In Chapter 19, you learned that it's important to beware of race conditions. A race condition is any type of situation where strange, inconsistent behavior might arise if multiple page requests try to change the values of the same variables at the same time. Race conditions aren't specific to ColdFusion development; all Web developers should bear them in mind. See Chapter 19 for more information about this important topic. Since these last few examples have encouraged you to consider storing instances of your CFCs in the APPLICATION or SESSION scopes, you may be wondering whether there is the possibility of logical race conditions occurring in your code, and whether you should use the <cflock> tag or some other means to protect against them if necessary. The basic answer is that packaging your code in a CFC doesn't make it more or less susceptible to race conditions. If the nature of the information you are accessing within a CFC's methods is such that it shouldn't be altered or accessed by two different page requests at the same time, you most likely should use the <cflock> tag to make sure one page request waits for the other before continuing. Direct Access to Shared Scopes from CFC Methods
If your CFC code is creating or accessing variables in the APPLICATION or SESSION scope directly (that is, if the words APPLICATION or SESSION appear in the body of your CFC's <cffunction> blocks), place <cflock> tags around those portions of the code. The <cflock> tags should appear inside the <cffunction> blocks, not around them. Additionally, you should probably place <cflock> tags around any initialization code (that is, within <cfcomponent> but outside any <cffunction> blocks) that refers to APPLICATION or SESSION. In either case, you would probably use scope="SESSION" or scope="APPLICATION" as appropriate; alternatively, you could use <cflock>'s NAME attribute as explained in Chapter 19 if you wanted finer-grained control over your locks. Also, ask yourself why you're even using the APPLICATION or SESSION scope in your CFC code. Is it really necessary? If the idea is to persist information, why not simply store the CFC itself in one of the persistent scopes? This will be helpful if you decide that the information needs to be specific to the SESSION and not to the APPLICATION. If you never directly referenced any scope in your CFC code, but instead simply stored the CFC in one of the scopes, "moving" the CFC then becomes a simple matter. Locking Access to the THIS Scope
The FilmRotationCFC example in this chapter (Listing 23.26) doesn't manipulate variables in the APPLICATION or SESSION scopes; instead, the CFC is designed so that entire instances of the CFC can be stored in the APPLICATION or SESSION scope (or the SERVER scope, for that matter) as the application's needs change over time. This is accomplished by only using variables in the THIS scope, rather than referring directly to SESSION or APPLICATION, within the CFC's methods. You may wonder how to approach locking in such a situation. I recommend that you create a unique lock name for each component when each instance is first instantiated. You can easily accomplish this with ColdFusion's CreateUUID() function. For instance, you could use a line like this in the component's initialization code, within the body of the <cfcomponent> tag: <cfset THIS.lockName = CreateUUID()>
The THIS.lockName variable (or property, if you prefer) is now guaranteed to be unique for each instance of the CFC, regardless of whether the component is stored in the APPLICATION or the SERVER scope. You can use this value as the name of a <cflock> tag within any of the CFC's methods. For instance, if you were working with a CFC called ShoppingCartCFC and creating a new method called addItemToCart(), you could structure it according to this basic outline: <cffunction name="addItemToCart"> <cflock name="#THIS.lockName#" type="Exclusive" timeout="10"> <!--- Changes to sensitive data in THIS scope goes here ---> </cflock> </cffunction>
In Chapter 28, a shopping-cart component is created that locks accesses to the THIS scope using this technique. Refer to the ShoppingCart.cfc example in that chapter for a complete example. For more information on the <cflock> tag, especially when to use type="Exclusive" or type="ReadOnly", see the "Using Locks to Protect Against Race Conditions" section in Chapter 19. |