DataMgr Components

The Challenges

Whenever I do a simple insert or update, I do several of the same simple checks.
Beyond that, the error messaged I get from the database often aren't very informative. Specifically, when the data is too long for a string field, the database generally doesn't indicate which field.

I also have a handful of reusable components and I want them to create any tables they need on any database I might use.

The Solution

Let's look at a sample "CritterMgr" component for handling, say, critters. This will be stored in CritterMgr.cfc in the root of our web site.

<cfcomponent>

<cffunction name="init" access="public" returntype="any" output="no"> <cfargument name="DataMgr" type="com.sebtools.DataMgr" required="yes"> <cfset variables.DataMgr = arguments.DataMgr> <cfset variables.DataMgr.loadXML(getDbXml(),true)>

<cfreturn this> </cffunction>

<cffunction name="setCritter" access="public" returntype="void" output="no"> <cfargument name="CritterName" type="string"> <cfargument name="BirthDate" type="date"> <cfargument name="CritterID" type="numeric" required="No">

<cfif StructKeyExists(arguments,"CritterID")> <cfset variables.DataMgr.updateRecord('Critters',arguments)> <cfelse> <cfset variables.DataMgr.insertRecord('Critters',arguments)> </cfif> </cffunction>

<cffunction name="getDbXml" access="private" returntype="string" output="no"> <cfset var tableXML = ""> <cfsavecontent variable="tableXML"> <tables> <table name="Critters"> <field ColumnName="CritterID" CF_DataType="CF_SQL_INTEGER" PrimaryKey="true" Increment="true" /> <field ColumnName="CritterName" CF_DataType="CF_SQL_VARCHAR" Length="60" /> <field ColumnName="BirthDate" CF_DataType="CF_SQL_Date" /> </table> </tables> </cfsavecontent> <cfreturn tableXML> </cffunction> </cfcomponent>

We will initialize CritterMgr with the init() method. Since that takes the DataMgr as an argument, we better initialize that first (Application scope is personal preference only):

<cfset Application.DataMgr = CreateObject("component","com.sebtools.DataMgr_MSSQL").init("mydsn")>

CritterMgr expects a type of "com.sebtools.DataMgr", but we are instantiating com.sebtools.DataMgr_MSSQL. This works because DataMgr_MSSQL extends DataMgr. Note that DataMgr components must be in com.sebtools. This can be in a com/sebtools under your CustomTags directory or under the root of your web site.

Now we can call the CritterMgr:

<cfset Application.Critter = CreateObject("component","CritterMgr").init(Application.DataMgr)>

CritterMgr's init() method takes the DataMgr and makes it available to the rest of the component in variables scope.

<cfset variables.DataMgr = arguments.DataMgr>

DataMgr must know the structure of any table that it works with. If the tables already exist, you have the option to simply pass the table name to the loadTable() method (note that DataMgr_Access does not yet support this).

Our CritterMgr, however, needs to define its own tables, so we use the loadXML() method of DataMgr. CritterMgr passes the XML to DataMgr's loadXML() and indicates true for table creation. In this example, the XML is retrieved from a private getDbXml() method.

<cfset variables.DataMgr.loadXML(getDbXml(),true)>

This should be a good example of the XML format. The XML Schema is also available. Note that the XML is case-sensitive.

In this example, we are creating tables in MS SQL Server (by using DataMgr_MSSQL). If we wanted to install the tables in a MySQL database we would have just called DataMgr using DataMgr_MySQL. DataMgr components exit for Access, MS SQL and MySQL (so far).

Next we need to add and update a records. Let's assume that we have a form with a BirthDate field and a CritterName field. The form will also contain a "CritterID" field if it is for an update. That being the case, here would be our code for an insert or update on the action page.

<cfset Application.Critter.setCritter(argumentCollection=form)>

Or

<cfinvoke component="#Application.Critter#" method="setCritter" argumentcollection="#form#"></cfinvoke>

Since the form scope is a structure, we can pass it into the setCritter method using argumentcollection. In this case, the form names match the argument names. Otherwise, we would have to specify each field instead of using argumentcollection.

The setCritter() method then calls insertRecord() or updateRecord() appropriately. It passes in the name of the table on which to perform the action and the structure of data to insert/update.

<cfif StructKeyExists(arguments,"CritterID")>
    <cfset variables.DataMgr.updateRecord('Critters',arguments)>
<cfelse>
    <cfset variables.DataMgr.insertRecord('Critters',arguments)>
</cfif>
For inserts and updates, DataMgr needs a structure indicating the data to insert or update with a key for each field with the value that you wish to insert/update. Since the arguments scope is a structure, we can pass it directly into our insert or update method. If the argument names didn't match the field names in the database, we would just need to create the appropriate structure in the method itself.

DataMgr will test each value for validity with the data-type of the field. It will throw ane error for any character field receiving more data than it can hold. Note that updates must include the primary key field or the DataMgr will throw an error.

Any fields missing from the insert/update method will not be included in the resulting query. So, if the BirthDate field isn't included in an update then it would keep the value it had before.

Conclusion

Now, inserts and updates are handled easily and with extra error-handling. Our CritterMgr can easily be installed on any database which DataMgr supports.

For more information, check out my CFCs page, which has documentation on DataMgr and downloadable files.

Leave me a note and let me know what you think!

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
I appreciate your excellent component but on mssql 2000 your loadtable method spikes my sql server and I get no result. After looking further into it I attempted running the get struct from table method in query analyzer and found that the column name where clause in the case statement causes it to hang. I don't have the code in front of me right now, but is there any reason this would happen? Thanks again for creating this!
# Posted By Kenton | 11/17/05 5:46 AM
Kenton,

I am sorry to hear that you are having trouble. Especially frustrating since my primary database is MS SQL 2000 and I haven't had any such problems.

I did just upload a new version of DataMgr yesterday (which I will blog about as soon as I find the time to do so). It is possible that the new version fixes your problem.

If not, please let me know so that I can attempt to investigate further.

Thanks!
# Posted By | 11/17/05 9:49 PM
I tried the latest version and have the same issue.

I don't know if you'd prefer I e-mail you, but here is what I've gathered from my investigation. When calling the loadTable method it eventually calls "getDBTableStruct". I've attempted just running the query in query analyzer, and the server will max out and I have to cancel the query. This is what I'm running:


   SELECT      COLUMN_NAME AS Field,
            DATA_TYPE AS Type,
            CHARACTER_MAXIMUM_LENGTH AS MaxLength,
            IS_NULLABLE AS AllowNulls,
            ColumnProperty( Object_ID('copyfileattachments'),COLUMN_NAME,'IsIdentity') AS IsIdentity,
            CASE
            WHEN EXISTS
               (
                  SELECT      Column_Name
                  FROM      INFORMATION_SCHEMA.TABLE_CONSTRAINTS
                  INNER JOIN   INFORMATION_SCHEMA.KEY_COLUMN_USAGE
                     ON      INFORMATION_SCHEMA.TABLE_CONSTRAINTS.CONSTRAINT_NAME = INFORMATION_SCHEMA.KEY_COLUMN_USAGE.CONSTRAINT_NAME
                  WHERE      INFORMATION_SCHEMA.TABLE_CONSTRAINTS.Table_Name = 'copyfileattachments'
                     AND      CONSTRAINT_TYPE = 'PRIMARY KEY'
                     AND      Column_Name = INFORMATION_SCHEMA.COLUMNS.COLUMN_NAME
               )
            THEN 1
            ELSE 0
            END AS IsPrimaryKey,
            Column_Default as 'Default'
   FROM      INFORMATION_SCHEMA.COLUMNS
   WHERE      table_name = 'copyfileattachments'
   ORDER BY   Ordinal_Position



So after messing with every part of the query I found that the problem line is this one:
AND      Column_Name = INFORMATION_SCHEMA.COLUMNS.COLUMN_NAME

If I go and change it to
AND      Column_Name = 'FileId'

It runs fine (of course it marks every column as primary since that column is.

I tried running the query with a hardcoded column name for every column and it never took more than half a second to run (I was hoping that one of the columns was messed up somehow.)

Hope this information is helpful I don't know what I could have done wrong!

Looking forward to using this, thanks!
# Posted By Kenton | 11/17/05 11:45 PM
By way of follow-up, this issue has been resolved.

The problem was my use of correlated subqueries (I know I should avoid them, but sometimes I succumb to temptation). As I will discuss in a future blog entry (as I find time), correlated subqueries can perform very badly for large data sets.

In any case, the newest verion of DataMgr (just uploaded), resolves the problem.
# Posted By | 11/18/05 9:37 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.