A couple weeks ago I blogged about a new data persistence object generation framework I have been working on. The Reactor framework allows you to generate customizable database abstraction objects on the fly with hardly any code at all.This release marks the 0.1 beta release. Over the past two weeks I’ve completely re-architected Reactor under the covers. Aside from one new method being added, the public API hasn’t changed at all. The re-architecting was done to facilitate more easily adding new database platforms and to more logically refactor and encapsulate code.
As a part of this process I separated the configuration settings into a bean which is passed in when creating a new instance of the reactorFactory. The purpose of this is to allow IoC frameworks such as ColdSpring and ChiliBeans to control the configuration of Reactor. Here’s an example of how you would instantiate the reactorFactory now:
<cfset config = CreateObject("Component", "reactor.bean.config").init("scratch", "mssql", "/reactorData", "always") /> <cfset reactor = CreateObject("Component", "reactor.reactorFactory").init(config) />
There is one rather important new feature: Beans. You may recall that the first release of Reactor had a feature called Beans. The old-style of beans were later renamed to Records to better reflect the purpose of these objects. I’ve now added a new and mostly-unrelated feature which lets you generate simple bean objects based on your tables. If you wanted to create a simple bean object based on a table in your database you could run this code:
<cfset InvoiceBean = reactor.createBean("Invoice") />
This code will create a simple bean object which has getters and setters for each of the fields in the Invoice table as well as an init method with optional arguments for each of the fields in the table. Because the bean methods accept arguments as strings they are well suited to back forms. In fact, you should be able to use these beans as event beans in Model-Glue and Mach-II.
Beans also have a validate method. When you call the validate method you pass in a reactor.util.validationErrorCollection object (which was stolen directly form Model-Glue). This object is populated with any errors and returned. The generated bean validation code checks that required fields are provided, that they are the correct type and that binary or string data are not too long.
Error messages are generated in English and stored in a file named ErrorMessages.xml is in the root of configured genereration directory. The structure of this XML file is quite simple. Here’s an example:
<tables> <table name="Invoice"> <column name="invoiceId"> <errorMessage message="The invoiceId field is required but was not provided." name="notProvided"/> <errorMessage message="The invoiceId field must be a numeric value." name="invalidType"/> </column> <column name="customerId"> <errorMessage message="Please select a customer for this invoice." name="notProvided"/> <errorMessage message="The customerId field must be a numeric value." name="invalidType"/> </column> <column name="dueDate"> <errorMessage message="The dueDate field is required but was not provided." name="notProvided"/> <errorMessage message="The dueDate field must be a date value." name="invalidType"/> </column> </table> </tables>
Each table you generate a bean for will generate table, column and errorMessage elements in this xml document. Error messages will only be generated if the message does not already exist. This means that you are safe to edit these error messages and customize them as you desire. As you can see above, I changed the error message for the customerId column from the default.
The obvious question now is what to do if you need to validate some sort of custom business logic? This is quite simple too. You can add a custom error message to the ErrorMessages.xml file for the table and colum. For example, this could be added to the invoice table’s dueDate column:
<errorMessage message="The Invoice Due Date must be no more than 30 days from today." name="dueDateTooFarOut"/>
This error message can now be easily be leveraged inside a custom validation method in the custom bean object. As you may recall, Reactor generates a base object as well as an empty shell which extends the base object and is used to customize generated code. The customizable object is never overwritten if it already exists, so it’s safe to edit.
You could customize the validate method in the custom InvoiceBean to validate that the Invoice’s due date is no more than 30 days from now. For example:
<cfcomponent extends="reactorData.Bean.mssql.base.InvoiceBean" hint="I am the custom Bean object for the Invoice table. I am generated, but not overwritten if I exist. You are safe to edit me."> <!--- Place custom code here, it will not be overwritten ---> <cffunction access="public" hint="I validate this object and populate and return a ValidationErrorCollection object." name="validate" output="false" returntype="reactor.util.ValidationErrorCollection"> <cfargument hint="I am the ValidationErrorCollection to populate." name="ValidationErrorCollection" required="yes" type="reactor.util.ValidationErrorCollection"/> <cfset ErrorManager=CreateObject("Component", "reactor.core.ErrorManager").init(expandPath("#getConfig().getCreationPath()#/ErrorMessages.xml")) var/> <cfset super.validate(arguments.ValidationErrorCollection)/> <!--- Add custom validation logic here, it will not be overwritten ---> <cfif 30 DateDiff("d", now(), getDueDate()) GT> <cfset ValidationErrorCollection.addError("dueDate", ErrorManager.getError("Invoice", "dueDate", "dueDateTooFarOut"))/> </cfif> <cfreturn arguments.ValidationErrorCollection/> </cffunction> </cfcomponent>
So, what good is a bean if it can’t be commited to the database? Well, the data can be. Remember how I said that the bean was backed by the same TO as Record objects? Well, the Record objects have a populate() method which accepts a Bean object. This method gets the TO from the Bean and sets it into the Record. (Beans also have a populate() method which works the same way in reverse.)
This means that if you had a fully populated and validated Bean you could easily populate a Record and save the data with this code:
<!--- code before this would create and populate the invoice bean ---> <cfset InvoiceRecord=reactor.createRecord("Invoice")/> <cfset InvoiceRecord.populate(InvoiceBean)/> <cfset InvoiceRecord.save()/>
Or if you had loaded a record and want to use it to back a form you could use those code instead:
<!--- code before this would create and load the invoice record ---> <cfset InvoiceBean=reactor.createBean("Invoice")/> <cfset InvoiceBean.populate(InvoiceRecord)/>
Here’s some other news:
I tested this on OSX, connecting to MSSQL on my Windows desktop. It worked great! This indicates that the system should be cross platform.
I’ve started writing a set of Unit Tests. These aren’t done. I’ll try to wrap these up within the next couple weeks.
Someone (who’s name I haven’t been given permission to use yet) has indicated that they will be adding support for MySQL. I’m hoping this happens quickly. I suppose that depends on him right now. I’d love to talk to other people about other databases, in particular Oracle.
Comments on: "Reactor For ColdFusion – Version 0.1" (30)
I have just done something vary similar (though not as sophisticated) for oracle, so wouldn’t be hard to add oracle support – I’ll give it a go after I’ve familiarised myself with your code.
Just wanted to thank you for your work on this project. I have been playing with it and have been both impressed and excited. Not sure if there is any way in which I can help, but feel free to let me know should I be qualified to assist in any manner.
Doug, maybe I am doing something wrong here, but I am getting some errors. First, if I set the mode to development, I get an error when my code runs after the first time – is this correct? I changed the mode to always and that works…however, when I call the init() method of my bean, I get a super.init() method does not exist. Am I confusing something here?
I’m getting the same error as Brian. The function readSuperTable in TableDao.cfc has to perform the create table in a seperate query before the insert into query. I thought a simple GO between the create and insert would do the trick but it didn’t.
I am sure you would have figured this out quickly, but it appears that the issue causing development mode to error out is in core/objectFactory line 80. The call to object.config() is missing the config argument…I changed it to <cfreturn Object.config(getConfig(),arguments.name, this) /> and it no longer errored. I am researching the error I get with the super call as well.
Thansk guys – in retrospect, I do most of my development in "always"… so this is probably an oversite on my part. Sorry for the problems. In a few days I’ll see what I can do to get this fixed.
Thanks Doug. Sorry to keep bugging you, but I have some time to test this tonight…I added an empty init method to base/abstractBean like so:
This was just to test since it appears that there was no super.init() function for my bean to call (since it inherited from abstractBean which was empty). Not sure what the init function was intended to do, but everything seems to work fine at this point even with just the empty init.
Brian – No problem bugging me… I just might not respond right away! 😉
Anyhow, what you’re saying makes sense. I need to edit the xsl for the base bean so that it only calls supper.init() when there is a super to call… at least that’s my understanding of what you’re saying. At the moment I don’t think the init method belongs in the abstractBean.cfc.
FYI – I (probably) fixed the problems both with development mode and with the init method in beans. I’m not yet packaging another version so this is up on SVN only.
Thanks Doug. I pulled the files and my initial tests worked.
ok. new issue. calling save seems to always call the update in the dao (my table is currently empty). Am I doing something incorrect? The record cfc seems to be expecting my primary key field to be empty? What happens if I am passing in the primary key for new records? or maybe I am misunderstanding…
Brian, In my code I assume (dangerous word!) that primary keys are identity values. Thus, the save method checks to see if the PK value is defined. If it is it tries to update. If it’s not it tries to insert.
If the table doesn’t have a primary key it always calls insert.
I’ve been trying to think of ways arround this. From time to time I use UUIDs for columns. On the other hand, I tend to set the default value to newId(), so I still wouldn’t run into this issue like you are.
One other thing I’ve concidered doing is adding an "existsInDb" property to the TOs. This would default to false. When the read method is called on the DAO this property would be set to true. Then, when you call save it’d look at that, rather that the existance/value of a PK column.
The thing is I tried this and ran into problems. I can’t think of what they were though.
Maybe I should just remove save and add two methods: update() and insert(). But frankly, that’s uglier and less elegant.
Maybe this isn’t the best method, but I usually have the save method in my DAO…it usually checks if the primary key value already exists and then decides to call my (private) insert or update methods. This would mean that the record would just call the DAO save method rather than put that logic in the record component. Is this helpful?
I think you’re right that the save logic should go in the DAO. The thing that still remains unclear is how the logic would determine if the TO needs to be inserted or updated.
I am going to give this some thought. For many cases I think having a existsInDb in the TOs would work, but I can imagine situations where the TO would not be explicitly set through a read method…so what would happen in those cases? Obviously, calling the read method to see if a record exists won’t work b/c it will overwrite the properties of the TO. In some cases I have written a private exists method that simply returns a boolean after checking the database against the pk.
Brian, I’m quite opposed (at the moment) to checking the DB for the existance of a PK. That seems like a lot more overhead than I’d want to put in…
I suppose that this might be a realistic option when the PK columns are not identity or don’t have defaulyt values. I’ll think on this.
For the time being in my test I simply defaulted the column to newid(). However, the insert still tries to insert the column even though the value is empty (since the default is in the db) causing an error. The error is "Syntax error converting from a character string to uniqueidentifier" – it is trying to insert is an empty string since I have not set a value to the id.
Brian, For data columns with a defauly of getdate() I’ve transalated that to a default value of now() for CF objects. I might be able to do the same thing for UUIDs. IE: translate defaults of newID() to createUUID().
Actually this weekend, I had tweaked the code to do just that (except transforming the CF UUIS to SQL format) but then ran into other issues once that was in place. One issue is that I get a SQL error regarding transforming data type char to uniqueidentifier. This will mean modifying those types as well…There were other issues too which I cannot remember at the moment but suffice it to say, it turned out to be a much larger change than I had anticipated.
Chances are that I would implement the transformation to and from CF uuids in the DAOs. I might even create a silly component to do this for me.
All in all, it needs some time and thought.
Thanks for continuing to put up with me on this, but I am thoroughly confused. So uniqueidentifier with newid() doesn’t work at the moment for the reasons discussed. Also, if I set it to an incrementing numeric id, the dao still tries to explicitly insert the id, which also fails. Lastly, if I set any sort of primary key without a default and try to pass in a value for the id via the bean, it always does an update… Am I doing something wrong? Also, just a reminder that I would be more than willing to do whatever would be helpful to you on this.
Brian – Right now, I’m going to consider this a bug. As time permits for me to return to this project I will see what I can do about this.
Thanks Doug. For the moment, I have made some modifications to the core files. I added a default for newid(). I made a save function in the dao (so save in the record only calls save in the dao). I know you disagree, but I went ahead and added an exists method to the dao (it pulls a maximum of one record and only the key colums in the hopes of limiting the load on the db). Anyway, if the changes I made are of any use to you, I can send them along.
Just so everyone knows, I’m not ignoring this feedback, I’ve just got to take care of some other stuff before I can get back to working on a 1.0 of reactor.
Brian, I’m currently using the previous version of reactor (the one without validation).
I changed a bit my dao.base and my record.base so it checks first if the record exists or not.
In my DAO I added a existsInDb property and exists function.
If you want just drop me and email and I’ll send you my 2 xls files. Don’t forget that those files are for the previous version.
joaofernandes at investec dot pt
FYI – I’ve started adding nicer support for both UUIDs and non-identy primary keys.
I’ve run into a few hangups related to the gateway object’s getByCriteria method and I’m reworking that a bit.
The last major feature I want to add is easy many-to-many relationships.
I hope to have something to show by the end of the week.
Also, I’ve downloaded the Oracle XE, SQL 2005, MySQL and Postgres. Guess where I’m headed next. 😉
I’m enjoying the reactor tool and the cool techniques used to create the compoments. I’m not sure if this is me in that I don’t fully understand component paths(or an assumption built into reactor) but when I create a config object with a path that is more than one folder deep I get the "/" character in the component name/definition.
results in return types of, for a table named document,
Adding the "all" attribute to the rereplace call in objectFactory.getObjectName, var creationPath = rereplaceNoCase…. seems to eliminate that. But causes an error when the same is done to the same call in the table.getCreationRoot function. I’m still tracking it down.
Thanks, I’ll keep that in mind moving forward.
Vote on Todays Current Topics & Recieve Prizes, which Include: Apple iPod, Nintendo Wii, Flat Screen TV and More!
drop by Paid Surveys
Survey of Music