The persistence service is a more traditional approach to ORM, compared to the ActiveRecord objects. With this approach, you write your objects separately from the ORM and then use the ORM only as a means transfering the object data between your objects and the database. The service also caches your objects in memory to reduce the amount of database activity and to ensure that there is only one copy of a specific object at any given time (in other words, it acts like a real-world object in that if two people interact with it at the same time, they're interacting with the same object). The persistence service will appeal to developers who use dependency injection (DI) frameworks like ColdSpring or LightWire. In fact, the persistence service is designed specifically to use your existing DI framework as a fundamental part of its operation.
- Sample Application
- Data Management
- Events and Listeners
- Registering Listeners
- Available Events
- Handling Events
- Delete Events
- One-to-Many and Many-to-Many Events
- Object Cache
The DataFaucet distribution includes a /mazegame directory which contains an application designed as a sample of the persistence service, which runs a simple maze game in which you can create a maze and then have your friends attempt to solve the maze. The rest of this persistence service documentation will reference this sample application.
There are five objects managed by the persistence service in this example:
|game.cfc||The Game object is a player's current attempt to solve a given maze |
the persistence service management of Game objects allows the player to save his game and return to it later.
|maze.cfc||The Maze object is a specific collection of rooms, obstacles and solutions, with a starting point and hopefully at least one exit (end point).|
|obstacle.cfc||Obstacles include doors, traps or other devices which might block the player's movement through the maze. |
Each obstacle may be found in one or more rooms and may have one or more solutions.
|room.cfc||Rooms define the shape of the maze and the placement of exits, obstacles and solutions.|
|solution.cfc||Solutions allow the player to overcome obstacles in the maze. |
Each solution is placed initially in one room in the maze and may apply to one or more obstacles.
Example: So if you're putting together a maze, you begin by creating a maze and the first room is automatically added in /create.cfm. The application then brings you to the editor where you see the first room (entrance) and can start adding rooms by clicking in the gray space around the entrance. After you've added a few rooms, click on a room and make it an exit. At this point the maze isn't very challenging. We can make it more challenging by adding a few obstacles. So we select "New Obstacle" from the editor's obstacle select box, which takes us to the obstacle editor. There we enter the name of our obstacle, select a few rooms and enter the name of our first solution. But at this point even though we've named our first solution, it doesn't exist in the maze yet, so we select the name of our solution from the editor's solution select box, which takes us to the solution editor. There we select the room where we want players to find our solution. As we continue to build our maze, game progresses we may decide that we want to add alternative solutions for obstacles that are already in the maze. So in addition to "ear plugs" we might also add a "chainsaw" solution, and both of them can be used to solve the Céline Dion obstacle. Once you're done creating the maze, then you can challenge your friends to solve it, but they'll have to do it quickly or they'll run out of time! Or if the boss cracks the whip, they can save their game and come back to it later.
In this example we have several relationships between a number of objects. These relationships are of three types: one-to-one, one-to-many and many-to-many. We have a Maze object, which has a one-to-many relationship to rooms, obstacles and solutions. This means that each Maze may have one or more of these other objects. The rooms in the maze on the other hand have a one-to-one relationship to the Maze, meaning that each room can only be associated with a single maze. Obstacles and solutions are a bit more interesting. Obstacles have a many-to-many relationship with solutions, meaning that each obstacle may have several solutions, and each solution may apply to several different obstacles. So for example a "wooden door" obstacle might be solved with a "silver key" solution, but it might also be solved by an "axe" solution. Similarly, while each door might have its own individual key, a "skeleton key" solution might apply to all the doors in the maze.
What's important to note about this example is that none of these five CFCs we created for the application are dependent upon the DataFaucet ORM. They are all generic CFCs and we've simply added a few hints to DataFaucet in the form of cfproperty tags to inform the persistence service about what data they contain and how it needs to be stored in the database. (There is an exception in the game.cfc in which it references the DataFaucet objects, however, this was only done for convenience. If this had not been a sample application, I would have found some other way for the game object to fetch rooms from the persistence service.)
The persistence service object is placed in your application scope, so that it's always available and it can cache your objects. When you create the persistence service and place it in your application scope, you'll need several other objects to make it work. These are the ClassCacheManager, the Class Factory and the DAO Factory. In practice, most of the time the Class Factory and the DAO Factory will both be a single instance of a dependency injection framework like ColdSpring or LightWire. To make this easier to demonstrate however, the Maze Game sample uses a couple of much simpler classfactory.cfc and daofactory.cfc objects. This is done in the onApplicationStart method of the Appplication.cfc with the following code.
<cffunction name="onApplicationStart" access="public" output="false"> <cfset var loc = structNew() /> <!--- change the attributes of this tag if you're using a different database server or if you want to change the name of the datasource ---> <cfmodule template="/datafaucet/open.cfm" datasource="datafaucet" server="mssql" catalog="datafaucet" schema="dbo" /> <!--- this creates the ORM system for use by the rest of the application ---> <cfscript> loc.df = request.datafaucet; loc.ds = loc.df.getDatasource(); // these are simple factories - in a typical application you would use // ColdSpring or Reactor instead of these factories loc.ClassFactory = CreateObject("component","datafaucet.system.classfactory").init("com.maze"); loc.DAOFactory = CreateObject("component","datafaucet.system.daofactory").init( Datasource = loc.ds, // DAO objects need a datasource Package = "com.maze", // this tells the DAO where the objects come from TablePattern = "tbl_*", // how should tables be named? AccessorPattern = "get*", // how are the object's getters named? MutatorPattern = "set*"); // how are the object's setters named? // the persistence factory must have a classcache manager loc.CacheManager = CreateObject("component","datafaucet.system.classcachemanager").init("application"); // now we create the persistence service application.orm = CreateObject("component","datafaucet.system.persistenceservice").init( CacheManager = loc.CacheManager, ClassFactory = loc.ClassFactory, DAOFactory = loc.DAOFactory); // this listener updates the datemodified column for all objects in the game when updated loc.listener = CreateObject("component","updatelistener").init(); application.orm.getBroadcaster().addListener("*","beforeUpdate",loc.listener); // make sure the tables exist in the database application.orm.install("maze,obstacle,solution,room,game"); </cfscript> </cffunction>
Here the DAO Factory is told some information about how we want to create our DAO objects. These DAO objects are responsible for all the database interactions for individual objects. So when we create, update or delete a Maze, the mazeDAO will write and execute all those queries. Internally when the persistence service later asks the DAO Factory for a specific DAO, like this:
<cfset dao = factory.getBean("mazeDAO") />
The factory then checks to see if it has the DAO object we requested and if not then it creates a new DAO with the desired properties. The DAO has these init arguments:
|Datasource||datasource||true||a datasource object to use for sql operations|
|ClassName||string||true||The full name of the class this DAO will populate|
|TableName||string||true||the name of the database table in which primary data for this class is stored|
|AccessorPattern||string||true||indicates the syntax for getting properties from these objects, i.e. getX() or getValue('x')|
|MutatorPattern||string||true||indicates the syntax for setting properties in these objects, i.e. setX(x) or setValue('x',x)|
|IDGenerator||any||false||an object that creates ID values for new database records|
So the DAO factory creates each DAO object much like this:
<cfset dao = CreateObject("component","datafaucet.system.dao").init(datasource,"com.maze.maze","tbl_Maze","get*","set*") />
These DAO objects are used sparingly however, because the objects are stored in cache and the DAO is only used when the service needs to read an object that isn't found in the cache, or an object is either deleted or saved to the database. So for example, when we call application.orm.get("Maze",1) the first time, it checks to see if it has Maze 1 in cache and when it doesn't find it there, it then gets a new object from the ClassFactory and turns to the DAO to read data from the database into that object and places the object in cache. When we later call application.orm.get("Maze",1) again, the service simply returns the object from the cache. Amidst these interactions between the cache and the DAOs, the service also uses the broadcaster to announce events before and after any object is saved to the database, deleted, or when object relationships change.
Here is a simplified map of most of the objects that are part of the persistence service object model. What you don't see in this map is the Datasource object, which is a required init argument and property of each of the DAO objects that are provided by the DI Factory. Of course, since the DI Factory will be supplying all these object, (i.e. the Maze and the DAO for the Maze), your DI framework will handle initializing the DAO with the appropriate Datasource object. In most cases, your DI factory will only have one Datasource object it uses for all the DAOs. Something else you don't see in this map is the ORMPrototype.cfc. This is an object the PersistenceService.cfc uses to provide extended ORM functionality to your objects. This is done with method injection at run-time, so the ORMPrototype.cfc isn't actually used as an object itself, it merely exists to provide functions that will enhance your CFCs.
When you then call PersistenceService.Get([className],x), the application flow between these objects looks like this:
In an object-oriented system, most objects have functions called Accessors (getters) and Mutators (setters). These functions are responsible for managing the objects data and communicating that data to other objects. In order for a Data Access Object (DAO) to store your object's data in the database or to later populate an object with data from the database, it needs to know how you've named your getters and setters. In our case we provide the DAO with two init arguments called AccessorPattern and MutatorPattern, which tell the DAO how to work with these methods.
For example if we have a Maze object with the methods getMazeID() and setMazeID() then we need to tell the DAO that our AccessorPattern is "get*" and our MutatorPattern is "set*". The DAO will replace the asterisk (*) in the pattern with the name of the property it's attempting to use.
The DAO object also supports generic getters and setters. So for example if you prefer to manipulate your properties with getValue("MazeID") and setValue("MazeID",666), you should use the AccessorPattern "getValue" and the MutatorPattern "setValue". When the DAO doesn't find an asterisk in the pattern, it switches to using the generic accessor / mutator syntax.
Okay that's cool, but how does it help me manage my data? I'm glad you asked. The persistence service object becomes a manager for your collection of objects so in any case in which you need to work with a specific object, instead of using CreateObject() or going directly to your Dependency Injection factory (i.e. ColdSpring or Lightwire), you request the object from the persistence service. Once you have the object, you can do whatever you like with it. It's only when you need to save changed to the object to the database that you then return to the persistence service. There are four basic functions for standard data management tasks: get, save, delete and undelete. The save method also makes use of a create method and an update method, but you don't really need to know about those methods.
|get(className [, id])||Gets a new or specific object with data loaded from the database|
|getArray(className, id)||Caches and returns an array of objects from the database - ID is a comma-delimited list of objects to get|
|getCollection(className, id)||Caches and returns a structure with objects from the database - ID is a comma-delimited list of objects to get|
|save(object [, inputCollection])||Updates the database with data from the object - performs a create or update as necessary.|
|delete(object [, id])||Removes one or more objects from the database and from cache.|
|undelete(object)||Reinserts a deleted record into the database|
|create(object)||Inserts a new record into the database (in most cases you should use Save instead)|
|update(object)||Updates the database record for a specific object (in most cases you should use Save instead)|
Example: When you create a new maze in the maze game, this code is executed (create.cfm).
<cfscript> maze = orm.get("maze"); maze.setName(attributes.name); room = orm.get("room"); room.setName("the Entrance"); room.setDescription("You've fallen through a trap door into a strange underground maze."); room.setMaze(maze); room.setX(1000); room.setY(1000); orm.save(room); // we don't have to save the maze in advance because it's a required property of the room </cfscript>
In this example, both the Maze object and the Room object are new objects. So we don't specify an ID when we request them from the service (orm.get()). When we then save the room object, the service automatically saves the maze because they're both new. If we later save the room object again, the maze object won't be simultaneously updated because it already exists in the database. When we then create a new room for this maze, we begin by specifying the ID of the new room's Maze like this (addroom.cfm):
<cfscript> maze = orm.get("maze",attributes.mazeid); room = orm.get("room"); room.setName("a darkened corridor"); room.setX(attributes.x); room.setY(attributes.y); room.setMaze(maze); orm.save(room); </cfscript>
If we later delete that room, there are two ways we can do that. We can use the object:
<cfset orm.delete(room) />
or we can specify a list of rooms to delete
<cfset orm.delete("room",attributes.RoomIDList) />
The persistence service will create all the necessary tables to store the data for your objects. All you need to do is call the install() method with a list of the classes you want to install. In fact, you have to call the install method if you don't want to have to manually create a WDDX packet with some configuration data. This is much easier, as seen at the end of the onApplicationStart event in the maze game's Application.cfc:
<cfset orm.install("maze,obstacle,solution,room,game") />
This function call will create all the necessary tables to store your objects and it will create the necessary configuration files for your DAO objects. Each element in the list is the name of one of the classes in the maze game to install. If you later want to remove both the tables and the DataFaucet config files for these objects, a similar uninstall() method will do that as well.
The persistence service gets its information about how to store your objects from the metadata in the object, specifically its CFPROPERTY tags. While the installer will make certain assumptions from common data types like "numeric", "string" and "date" (integer, nvarchar and timestamp respectively), greater control of your column definition is available with an optional length attribute in your property. So for example adding length="10" to a string property will create a varchar(10) column in your database. For string columns, adding an exclamation point (!) after the length will make the column fixed width (char instead of varchar), or adding an asterisk (*) after the length will make the column an international nchar or nvarchar column (containing multi-byte unicode characters). Character Large Object (CLOB), text or memo columns can be created by specifying a length of "long" or "long*" for unicode.
Here is a list of common column types you might want and how to specify them in your properties.
This table shows how to define various column types:
|Data Type||Property Definition|
|integer||<cfproperty name="NumPages" type="numeric" required="false" />|
|numeric(3,2)||<cfproperty name="MyFloat" type="numeric" required="false" length="3,2" />|
|real||<cfproperty name="MyReal" type="numeric" required="false" length="real" />|
|tinyint||<cfproperty name="NumChildren" type="numeric" required="false" length="tinyint" />|
|char(10)||<cfproperty name="ssn" type="string" required="false" length="10!" />|
|nchar(10)||<cfproperty name="ssn" type="string" required="false" length="10!*" />|
|nvarchar(50)||<cfproperty name="name" type="string" required="false" />|
|nvarchar(1000)||<cfproperty name="notes" type="string" required="false" length="1000*" />|
|varchar(35)||<cfproperty name="id" type="uuid" required="true" />|
|varchar(50)||<cfproperty name="name" type="string" required="false" length="50" />|
|timestamp||<cfproperty name="datecreated" type="date" required="false" default="now" />|
|autonumber||<cfproperty name="myid" type="numeric" key="true" autonumber="true" />|
|long text||<cfproperty name="TheWholeChapter" type="string" required="false" length="long*" />|
|bit||<cfproperty name="isDeleted" type="boolean" required="true" default="0" />|
|NOTE: Individual database engines may make data type substitutions as appropriate for a given database. For example, MySQL doesn't support "long text" and so will revert to a 2000 character varchar column. Similarly Oracle will make a substitution for a data type of "money". This is not an exhaustive list of exceptions.|
Here is an example of some properties from the maze game. Note that the Obstacle object inherits some properties from base.cfc, which is never installed by the persistence service, existing solely to provide some generic or universal functionality to other objects.
<cfcomponent output="false" displayname="base.cfc"> <cfproperty name="datecreated" type="date" required="false" /> <cfproperty name="datemodified" type="date" required="false" /> <cfproperty name="name" type="string" required="true" /> <cfproperty name="description" type="string" required="false" length="500*" /> <cfset variables.instance = structNew() /> <cfset instance.datecreated = now() /> <cfset instance.datemodified = now() /> ... </cfcomponent> <cfcomponent output="false" displayname="obstacle.cfc" extends="base"> <cfproperty name="obstacleid" type="uuid" key="true" /> <cfproperty name="maze" type="maze" required="true" /> <cfproperty name="roomArray" type="array" required="false" references="room" /> <cfproperty name="solutionArray" type="array" required="false" references="solution" xref="tbl_xObstacleSolution" /> <cfproperty name="obstacletype" type="string" required="true" length="4" /> <cfproperty name="obstacleimage" type="string" required="false" /> <cfproperty name="wall" type="string" required="true" default="N" length="1" /> <cfproperty name="color" type="string" required="false" length="6" /> ... </cfcomponent>
If you need more control over the tables that are created for your object, you can create XML to describe the tables yourself. This XML does not replace the need for CFPROPERTY tags when using the persistence service, but supplements them instead, giving you more control of the database structure. This is done by providing a ConfigPath argument when initializing the persistence service object (persistenceservice.cfc). This argument is an absolute path to a directory containing XML files with the DDL for each table to install. So for example if the Maze object (maze.cfc) stores most of its data in a table named tbl_Maze, then the config directory should contain a file named "tbl_maze.xml.cfm". This file may contain DDL for additional tables, such as crossreference tables, but must at least contain DDL for the primary table.
The maze game sample does not use these optional config files - when no matching file is found in the config directory, the installer reverts to using the object metadata (CFPROPERTY tags) to build the DDL for tables that will store your class data. The following example shows DDL that will alternatively create an autonumber column for databases that support them OR create a sequence for databases that support sequences. The unsupported feature (sequence or autonumber) is ignored by the target database engine when the object is installed.
<ddl> <create type="table" name="#getTable()#"> <col name="productid" type="integer" autonumber="true" key="true" /> <col name="productname" type="nvarchar(20)" null="false" /> <col name="productdescription" type="nlongvarchar" /> <col name="productprice" type="real" required="true" /> <!--- create a foreign key constraint to ensure this product is placed in a category ---> <col name="categoryid" type="varchar(35)" required="true" references="tblProductCategory.CategoryID" /> </create> <create type="sequence" name="seqProduct" /> </ddl>
Your objects will need to have a primary key column defined in their CFPROPERTY tags. This lets the installer and the DAO know how to identify your objects. There is no support for multi-column primary keys on object tables. By default the DAO will supply a UUID value as a new ID for objects the first time they are saved to the database. You can override the default ID values by supplying an IDGenerator CFC component when you create and initialize the DAO object. There are two such ID Generator objects supplied with DataFaucet out of the box, the default generator for UUID values and an alternative ID Generator for sequences. With ColdFusion 8 or later, you can also use autonumbers or identity columns if your database supports them by specifying autonumber="true" in the CFPROPERTY tag for your primary key column. It is not necessary to supply an alternative ID Generator CFC when using an autonumber primary key.
In the maze game sample application, each CFC has its own primary key property (although name and description are inherited from base.cfc). So for example, Maze.getMazeID() returns the ID of a specific maze object. And the maze.cfc contains this CFPROPERTY tag to create this primary key.
<cfproperty name="mazeid" type="uuid" key="true" />
When we're creating an Object-Oriented application individual objects often have relationships to other objects. So for example if we're modeling a family, an individual Persion object might have a spouse, and you could return the spouse from that object using Person.getSpouse(). These kinds of relationships are divided into three categories: one-to-one, one-to-many and many-to-many.
One-to-One: A one-to-one relationship is like the relationship between the Person object and their Father. The object references a single other object. The related object may or may not have a reciprocal relationship. For example, while you might be able to Task.getProject(), that doesn't necessarily mean you can also Project.getTask(). In the maze game sample application there are Rooms, Obstacles and Solutions, each of which must be associated with a single Maze. So each of these objects can Room.getMaze(), Obstacle.getMaze() or Solution.getMaze().
One-to-Many: When one object has a one-to-one relationship, the other object may also have a reciprocal one-to-one relationship (for example Person.getSpouse() may create a perfectly circular relationship), or it might have a one-to-many relationship. For example, while you can Person.getFather(), the Father may have many children, in which case you may Father.getChildren() but not Father.getChild(). In the maze game example, each Maze has one or more Rooms, Obstacles and Solutions and may Maze.getRooms(), Maze.getObstacles() or Maze.getSolutions(), with each function returning an array of the appropriate objects.
Many-to-Many: A many-to-many relationship occurs when two types of objects have a reciprocal one-to-many relationship. In the maze game sample application, the mazes are filled with obstacles. Each obstacle has a one-to-many relationship to the rooms (it can be placed in several rooms), but the rooms have only a one-to-one relationship to the obstacle (i.e. room.getObstacle()). In order for a player to overcome an obstacle, they must find a solution that matches that obstacle, however, you can assign multiple solutions to a single obstacle. So for example, a Wooden Door might have a Brass Key solution, but it might also have an Axe solution at the same time. The reason this is a many-to-many relationship and not a one-to-many relationship is because the solutions may also apply to multiple obstacles. So for example there might be several different kinds of doors, each with their own key, and then a Skeleton Key solution might also apply to all the doors.
These relationships are also known as "composition" or "aggreagation". The difference between composition and aggregation is that composition implies ownership. So a maze is a composition of rooms, meaning that, if you delete the maze, all the rooms are also deleted. An obstacle in the maze however has an aggregate of solutions, because its solutions remain even after you delete the obstacle. While a many-to-many relationship is always an aggregate, one-to-one and one-to-many relationships are usually composition, implying ownership.
Managing these relationships with the persistence service is relatively easy. Each relationship you want to manage must be declared in your object CFC using a CFPROPERTY tag, just like the other properties of your object. For example, the Room object in the maze game sample application must have a maze, so it has a Maze property that looks like this:
<cfproperty name="maze" type="maze" required="false" />
The persistence service assumes that the type is the name of the class, just as you would call it from your Dependency Injection Factory (ColdSpring or LightWire). So although the full class name of the maze object is "com.maze.maze", we simply define the property type as "maze", because we would get one from the factory using factory.getBean("maze"). Once we have this property, we can now call Room.getMaze() and get back the Maze object associated with our Room.
Simply specifying the type works perfectly for one-to-one relationships however, for one-to-many or many-to-many relationships we must provide some extra information. The type for one-to-many and many-to-many relationships should both be "array", meaning that when you get these properties, you will receive an array of the composed or aggregated objects. So for example, the Maze object in the maze game sample application has a Rooms property that looks like this:
<cfproperty name="Rooms" type="array" required="false" references="room" />
Here we've indicated that the property is an array, so in order to indicate the type of objects in that array, we add the references attribute and give it the same class name value we previously placed in the type attribute for the one-to-one relationship. So here we specify references="room" because we would get a room object from our Dependency Injection Factory with factory.getBean("room"), even though the full class name is "com.maze.room". Once we have this property, then we can get an array of the rooms in our Maze with Maze.getRooms().
It is also possible to define a one-to-many property that returns a collection instead of an array by defining the cfproperty tag with a type of "struct".
<cfproperty name="Rooms" type="struct" required="false" references="room" />
If the room object had multiple objects of type "Maze" (I'm not sure why they would), then you may need to specify which property this array will reference. So for example, if Room had these properties:
<cfproperty name="Maze" type="maze" required="false" /> <cfproperty name="altMaze" type="maze" required="false" />
Then you might need to specify your rooms property in the Maze object this way:
<cfproperty name="Rooms" type="array" required="false" references="room.Maze" /> <cfproperty name="AltRooms" type="array" required="false" references="room.altMaze" />
Many-to-Many relationships are again slightly more complicated. These relationships can only be managed in the database by creating a crossreference table which contains foreign keys matching the primary keys in both object tables. So for example, in the maze game sample application, there is a tbl_xObjectSolution table in the database to store the relationship between objects and solutions. This is specified in the CFPROPERTY tag by adding the name of the crossreference table in an xref attribute like this:
<cfproperty name="solutionArray" type="array" required="false" references="solution" xref="tbl_xObstacleSolution" />
Once this property is in our object, we can then get the array of solutions for a given obstacle by calling Obstacle.getSolutionArray(). And of course the solution object has a similar obstacleArray property, so we can also call solution.getObstacleArray() to get the array of obstacles for that solution.
Although you'll be creating and updating your objects with the Save() method, it's sometimes easier to make changes to your objects in a more generic manner. In the maze game sample application for example, all the objects extend a base.cfc which provides several default properties for all the objects in the game. These include name and description, but they also include a DateCreated and a DateModified property. It's easy enough to supply the DateCreated value by setting it when we create the object however, we want the DateModified value to change any time the object is saved to the database, to reflect when it was changed. This is a bit trickier. Because your object is not part of DataFaucet and doesn't extend any of the DataFaucet objects, it doesn't know when it's being saved to the database. That means that if we want to update the DateModified property each time we save on of these objects, we can either manually update it, or we can find some way for the persistence service to automatically update it from the outside. Manually updating it would create a lot of maintenance for us, so the optimal solution is to find a way for the service to automatically update it. That method is with events and listeners.
A listener is simply a CFC that registers with the persistence service and executes some code when the service announces a particular event. So in our example, we need to create a CFC that can register and listen for the the "beforeUpdate" event, to update the object with the new date whenever the object is saved to the database. You can see this in the setup example above from our Application.cfc onRequestStart method, near the bottom:
<cfscript> // this listener updates the datemodified column for all objects in the game when updated loc.listener = CreateObject("component","updatelistener").init(); application.orm.getBroadcaster().addListener("*","beforeUpdate",loc.listener); </cfscript>
All of the persistence service listeners register and receive their announcements from the broacaster object (persistencebroadcaster.cfc). Once this code is in place, then each time we call orm.Save(object), the broadcaster will automatically call the Respond() method on our new updatelistener.cfc.
Of course this wouldn't be nearly so useful if it were limited to just the Respond() method, so we can change the name of the method to execute by supplying an additional argument to the addListener() method. Here are all the arguments for the addListener method:
|className||string||false||names the class of objects this listener will receive notifications for - use * to listen for all objects|
|eventName||string||false||the name of the event this listener will receive notifications for|
|listener||any||true||a CFC object that will receive the notifications|
|methodName||string||false||the name of the method to call on the listener when the event occurs - defaults to RESPOND|
In the example above, we only specified a listener for *, which means that object listens and responds to the beforeUpdate event on all objects. If we want to respond to events on specific objects, then we replace the * with the name of the object class as it would be called from our Dependency Injection framework (ColdSpring, Lightwire, etc.). So in the maze game I did just that, adding the same listener, with a different response method for several additional methods on specific objects; Rooms, Obstacles and Solutions.
<cfscript> // this listener updates the datemodified column for all objects in the game when updated loc.broadcaster = application.orm.getBroadcaster(); loc.listener = CreateObject("component","com.updatelistener").init(application.orm); loc.broadcaster.addListener("*","beforeUpdate",loc.listener); // these additional event notifications update the DateModified property of the Maze // any time we add or modify other objects in the maze // delete would have been more complicated and I didn't include it loc.broadcaster.addListener("room","afterUpdate",loc.listener,"updateMaze"); loc.broadcaster.addListener("room","afterInsert",loc.listener,"updateMaze"); loc.broadcaster.addListener("obstacle","afterUpdate",loc.listener,"updateMaze"); loc.broadcaster.addListener("obstacle","afterInsert",loc.listener,"updateMaze"); loc.broadcaster.addListener("solution","afterUpdate",loc.listener,"updateMaze"); loc.broadcaster.addListener("solution","afterInsert",loc.listener,"updateMaze"); </cfscript>
The system currently announces the following events:
|BeforeInsert||before the first time an object is saved|
|AfterInsert||after the first time an object is saved|
|BeforeUpdate||before an existing object is saved to the database|
|AfterUpdate||after an existing object is saved to the database|
|AfterRead||after the first time an existing object is read from the database and placed in cache|
|BeforeDelete||before one or more objects are deleted|
|AfterDelete||after one or more objects are deleted|
|BeforeUndelete||before an object is undeleted|
|AfterUndelete||after an object is undeleted|
|BeforeOneToManyUpdate||before setting a new array of objects on an owning object|
|AfterOneToManyUpdate||after setting a new array of objects on an owning object|
|BeforeManyToManyUpdate||before setting a new array of objects on an owning object|
|AfterManyToManyUpdate||after setting a new array of objects on an owning object|
When the listener method is called, it will then be supplied with these named arguments which it can use to perform its task:
|object||object||the object on which the event occurs|
|[classname]*||object||synonymous with the object argument|
|eventName||string||the name of the event that's occurring|
|className||string||the name of the class of object as called from your DI framework (i.e. ColdSpring)|
|detail||string||additional information for specific types of events|
|* i.e. if the event occurs on a Room object, you can use Room.setDrapes() instead of Object.setDrapes()|
In our example, we only need to update the DateModified property of the object, so this task is very simple to perform:
<cffunction name="respond" access="public" output="false"> <cfargument name="object" type="any" required="true" /> <cfset object.setDateModified(now()) /> </cffunction>
Because it is sometimes necessary to delete several objects at the same time, the beforeDelete and afterDelete events provide a slightly different announcement. In those cases, the object argument passed to the listener method is a comma-delimited list of the ID values of the objects that are deleted. This means that if the delete listener must make changes to another kind of object, for example, if the Room listener needed to modify the Maze when rooms are deleted, then it needs to access the database directly to determine which related objects are affected.
It's also important to note that delete operations automatically cascade. In the case of the maze game sample application, all rooms, obstacles and solutions have a required Maze property. Because this property is required, all rooms, obstacles and solutions associated with the Maze are automatically deleted before deleting the Maze. (This is called "composition", i.e. the Maze "is composed of" Rooms, Obstalcles and Solutions.) If the Maze property were not required, then these objects would remain after the Maze is deleted. (This is called "aggregation", i.e. the Maze "has an aggregate" of the objects that remain after it is deleted.)
The Detail argument supplied to event listeners is an empty string for most events. This argument is intended to supply additional information for specific types of events that might require additional special handling. In the current version, the only events that supply any detail information are BeforeOneToManyUpdate, AfterOneToManyUpdate, BeforeManyToManyUpdate and AfterManyToManyUpdate. These events announce when a stored object receives a new array for an array property and the relationship between the primary object and the objects in the array is then updated in the database. The value supplied in the detail is the name of the property being set, followed by a list of the ID values of the objects in the array, separated by a colon. So for example, if we supplied a new array of solutions for an obstacle in our maze game, the service announces BeforeManyToManyUpdate with the Obstacle object and the detail argument might look like this: "SolutionArray:3,8,12".
One of the tasks of the persistence service is to ensure that there is only one copy of a specific object at any given time. To do this the service object (persistenceservice.cfc) needs to keep these objects in memory and keep track of them. But it also needs to be able to manage them in a way that allows them to be expired when they've become idle and the server needs to reclaim space in memory. This latest version of DataFaucet takes advantage of a new open source project called CacheBox, which is designed to be a universal cache-management application that will allow you to assign cache to different mediums (ehCache, memcached, disk, etc) in a hot-swappable manner at run-time, as well as providing a convenient management application to make managing your cache easier. Only a small portion of the CacheBox project, the CacheBoxAgent (CFC) is included in DataFaucet. This provides only a very basic level of caching functionality, so to get the advanced features in CacheBox, you'll need to download and install CacheBox separately. It should be as simple as extracting CacheBox to your webroot.