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.