[coldbox-3.8.1] Application architecture - ORM & non-ORM models working together

ColdBox gurus, what approach should I take if I want to define an object (in model directory) that is not persisted to the database, but want to inject a couple entityService elements into it for objects that are persisted? I don’t even know how to ask this question properly yet…and I’m sorry for the long post. Basically…I just need to know if I have the basic architecture down and if not, what new components I need to add/adjust. Thanks so much!

An example scenario is imagine I want to allow the application user to search for customer information, however behind the scenes there are:

  • Two separately managed database tables which store the customer information (Imagine one table from a CRM and another from a non-integrated Sales system). Each table has some information we’d like to display in the search results.
  • Though the systems are separately managed, most customers have records in both systems and have a unique identifier, loyaltyID, which is the field we’d use to “merge” the results.

If for example, a user searches for loyaltyID ‘1234’, I imagine being able to lookup the information from both tables (might get a single result from both tables, or just one table, or none), merge it together into one generic Customer object (not mapped/persisted to the database) and loop through an array of these generic Customer objects within the View to present the user with the results. Here’s what I had in mind:

CRMCustomer.cfc (mapped to backend CRM table)

`
component persistent=“true” table=“CRMCustomer” {
// Primary Key
property name=“loyaltyID” fieldtype=“id” column=“loyaltyID” generator=“native” setter=“false”;

// Properties
property name=“name” column=“Name” ormtype=“string”;
property name=“phoneNumber” column=“PhoneNumber” ormtype=“string”;
property name=“address” column=“Address” ormtype=“string”;
property name=“gender” column=“Gender” ormtype=“string”;
property name=“accountNumber” column=“AccountNumber” ormtype=“string”;
}
`

SalesCustomer.cfc (mapped to backend Sales table)

`
component persistent=“true” table=“SalesCustomer” {
// Primary Key
property name=“loyaltyID” fieldtype=“id” column=“loyaltyID” generator=“native” setter=“false”;

// Properties
property name=“name” column=“Name” ormtype=“string”;
property name=“PIN” column=“PIN” ormtype=“string”;
property name=“phoneNumber” column=“PhoneNumber” ormtype=“string”;
property name=“lastPurchase” column=“LastPurchase” ormtype=“date”;
}
`

Customer.cfc (not persisted, represents a single customer with properties set from both tables)

`
component output=“false” accessors=“true” {
// DI Injections
property name=“CRMCustomerService” inject=“entityService:CRMCustomer”;
property name=“SalesCustomerService” inject=“entityService:SalesCustomer”;

// Properties
property name=“loyaltyID” type=“int”;
property name=“name” type=“string”; //originated from CRM
property name=“phoneNumber” type=“string”; //originated from CRM
property name=“address” type=“string”; //originated from CRM
property name=“gender” type=“string”; // originated from CRM
property name=“accountNumber” type=“string”; // originated from CRM
property name=“PIN” type=“string”; // originated from Sales
property name=“lastPurchase” type=“date”; // originated from Sales

//User Functions
/Build some functions here to use the Virtual Entity Services above and place the data into the properties where I want it to go/
}
`

I don’t see any reason why you can’t take to results and merge them together as one. First question-- are both these tables available via the same data source? Using ORM with multiple datasources is possible but requires some extra work. (Not something I personally have experience with)

One of my first thoughts is to create a base customer object that represents a customer–regardless of the system they’re stored. Then extend that base object with your specific entities. Then you can reduce redundant code and have a consistent API that the application expects.

As far as the process for loading from both systems and combining, I don’t think that logic will live in any of your customer bean implementations-- that’s something for a service of your design. You can create ColdBox entity services for both “types” of customers, but I still think you’ll need some “master” CustomerService that knows about both of them and is able to talk to each to get their results and combine them.

Whether you get an array of objects back from each and append the array, or get results back, combine them and then populate an array of objects with that result set is up to you. I’d play around with a couple approaches and see what works best.

Thanks!

~Brad

ColdBox Platform Evangelist
Ortus Solutions, Corp

E-mail: brad@coldbox.org
ColdBox Platform: http://www.coldbox.org
Blog: http://www.codersrevolution.com

Brad,

Not sure where you see different datasources, I took the wording as two tables.

@OP
Personally I would look at creating a relationship between the two. Other than that, create a bean that requires both information that is populated when your service retrieves the data.

If you have control over the database, then relationships is the best. Remember that if CF-ORM is never going to be set to update in the Application.cfc, the relationships are created on the fly with hibernate, therefor the database tables technically don’t need to have the relationship in the actual tables. But there is a performance hit for doing that, in most cases that might not be an issue. But just an FYI.

Brad and Andrew, first of all thank you. I honestly don’t know what I’d do without some community help while trying to get started with CB. Ok, a couple follow-up questions for you, if time permits.

@Brad, yes these are on the same datasource. Also, If I understood correctly, instead of placing user defined functions to do the data “merging” in the generic Customer.cfc, I could use a CustomerService.cfc that would look something like this?

CustomerService.cfc

`
component singleton {
// DI Injections
property name=“CRMCustomerService” inject=“entityService:CRMCustomer”;
property name=“SalesCustomerService” inject=“entityService:SalesCustomer”;
property name=“Customer” inject; ← ****** DO I EVEN NEED THIS?

// Constructor
public CustomerService function init(){
return this;
}

// New Customer
public Customer function new() {
return new Customer();
}

// Populate Customer Object our special way, joining a record from two different tables
public Customer populateCustomer(Customer theCustomer) {
//retrieve data from CRM
var c = CRMCustomerService.newCriteria();
c = c.restrictions.eq(“loyaltyID”, theCustomer.getLoyaltyID()).list(); //get our results from CRM here based on loyaltyID

//retrieve data from Sales
var d = SalesCustomerService.newCriteria();
d = d.restrictions.eq(“loyaltyID”, theCustomer.getLoyaltyID()).list(); //get our results from Sales here based on loyaltyID

/* some special merging/setter logic here */
theCustomer.setAddress(someAddrVar);
theCustomer.setPhoneNumber(somePhoneVar);
…other properties

//return the newly populated Customer
return theCustomer;
}

// Other functions that work with the CRM and Sales Services and manipulate the Location object passed to the function.
}
`

@Andrew, when you mentioned creating relationships, did you mean at the database level, or at the ORM level? It’s kind of one of those situations where there can’t be a “constraint” of sorts because of the standalone nature of the two tables from disparate systems. Ugly. I did try at one point to define a one-to-one relationship at the CF-ORM level but I had problems because the property used for joining, loyaltyID in this case, was used in other ORM relationships and CF was angry about using it again in a one-to-one.

When you mentioned alternatively to create a bean that requires both information that is populated when your service retrieves the data, did you have something in mind like I posted above? Sorry, I honestly can’t tell if I’m an inch or a mile away sometimes. I am getting some promising results as I implement these suggestions into my code though, just angling for the most wise approach :slight_smile:

Thanks again guys!

Wes

I think this is a hack but in legacy database tables it is a godsend at times, but you could use composite keys. I would setup an ID that is unique to that table, then use them as composite keys. The problem here is that unless this has changed, which I will admit I haven’t looked into it, composite keys did cause me issues in ColdFusion 9 and have since stayed away from them.

And yes I mean at the ORM level, that way the Database can stay intact and never get modified, provided that the Application settings are always defined as none.

As for the bean, yes along the lines you describe and the injection is what I would do. Writing a service to populate these, or even a model would work. However I would look at this as the last resort, as it would be more beneficial at the DB layer to create a relationship. I am assuming that an extra column would not be hard to implement at the table level, if it is something that can’t be done for whatever reason then I would continue the way your going and use ColdBox populate model to then populate the two Entities into the bean you have already defined.

Thanks again, Andrew. You and Brad helped me immensely and I am extremely grateful!

Wes