Short answer: don't do that.
But if you have:
function foo(rc) {
...
}
then you *could* do:
function bar(rc) {
...
foo(rc);
...
}
I strongly discourage doing so, however.
Controllers should never call other controllers. If there is common
code, factor it into your service layer and call it from both
controllers instead. It sounds to me like you're just trying to make
your controller methods too "fat". They should be very simple. The
"work" should all be in your Model, in the service layer for
coordination of objects and in the business objects themselves.
> I miss my Fusebox's DO verb.
You want "do" service methods instead of controller methods. In a
large project, I'd also avoid service() and manage service calls
directly (use ColdSpring or DI/1 to manage object life cycles and auto
wiring).
To avoid a lot of boring CRUD work, you might want to look at ORM
(assuming you're on Railo or ACF9/10). I personally don't like ORMs***
but it will save you a boatload of tedious coding.
Hope that helps?
*** Like Ted Neward and Jeff Atwood, I view ORMs as the "Vietnam of
computer science":
and:
http://www.codinghorror.com/blog/2004/07/why-objects-suck.html
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/
"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)
Sean
> --
> FW/1 on RIAForge: http://fw1.riaforge.org/
>
> FW/1 on github: http://github.com/seancorfield/fw1
>
> FW/1 on Google Groups: http://groups.google.com/group/framework-one
Ah, I'm with you now!
There are two things that are "bad" about ORM:
1) they traffic in objects (when structs or queries are more efficient
/ more effective)
2) they manage relationships between objects (so they tend to do some
very inefficient database fetches)
At World Singles, we've created a thin wrapper around JDBC that
traffics in structs (and arrays of structs). Our back end is
increasingly developed in Clojure now, which has maps (structs),
vectors (arrays) and sequences (also effectively arrays), so using the
Clojure JDBC library makes sense for us.
We also have a very thin generic "bean" CFC that acts as an Iterating
Business Object. This allows us to wrap an object-like API over a
result set, even if it is a join across multiple tables. We can
provide a specific CFC, to override the generic bean, so we have
calculated methods as well as getter / setter methods.
This gives us control over retrieving related objects so we can
optimize that (with custom SQL and joins) but also gives us the
flexibility of objects without the overhead. The combination of a few
thin layers means we can interact at different levels of abstraction
depending on the efficiency we need (from Clojure code to CFML structs
to CFML objects).
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
The latter. It's handled in the Clojure JDBC library but it's easy
enough to write a generic CFML version that generates an INSERT or
UPDATE based on the keys in the struct you pass in. There's some
subtleties around PK handling but if you have a couple of simple
conventions, it falls into place easily (we use ID in all tables and
it's auto-generated by the DB; so a struct with an ID key is an UPDATE
and a struct without an ID key is an INSERT). You also need to
remember dirty data vs loaded data but that's easy with two structs:
getFoo() checks dirty.foo then loaded.foo; setFoo() writes to
dirty.foo; save() uses the dirty struct for INSERT/UPDATE.
ORM is for Domain Objects, not services. I generally have my Domain
Objects under /model/beans (and my service CFCs under /model/services
because I manage my service CFCs using a DI framework).
> After CFWheels I'm really suspicious when it comes to acronym "ORM".
Oh? I've never used CFWheels (I'm not a fan of full-stack frameworks,
in case folks hadn't guessed). Did you run into problems?
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
Sort of. The underlying JDBC driver actually verifies / coerces the
types to some degree but you can't pass a string ("123") where an
integer is expected. Luckily Railo actually keeps numbers as numbers
and dates as dates (instead of converting back and forth to strings
like ACF does) so interop between Railo and Clojure is seamless and
when you pass data down to the JDBC layer, it's already the right
type. It does mean that you need to be a bit more careful about types
in your own code, e.g., calling val() on form & URL variables that are
supposed to be numeric and parseDateTime() or similar for dates, but
that's just good practice anyway - and should make your code run
faster.
> The only way I can think to deal with sql type if the approach you describe
> were implemented in CF would be some sort of hungarian notation
Ugh! Horrible idea. Having worked with the Clojure wrapper around
JDBC, I actually find CFML's cfqueryparam stuff to be horribly bulky
and redundant - the driver knows what types the columns are so a
simple parameterized SQL statement should not need any additional type
information. Here's a typical SQL query:
variables.orm.createIterator(
name = "user",
sql = "SELECT * FROM user WHERE siteId = ? AND username LIKE ?",
params = [ siteId, pattern ]
);
That calls orm.execute( sql, params ) - which drops down thru Clojure
to a PreparedStatement in Java - and returns a sequence of maps (think
array of structs in CFML). The name parameter specifies what bean type
to use (i.e,. which CFC to instantiate) and that is initialized with
the sequence and marked as an iterator. Standard methods hasMore() and
getNext() implement the iterator pattern. The getters return
data[currentRow][colName]. Setter update the dirty struct. Calling
save() performs an update based on the dirty struct.
There is no code generation here, BTW.
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
The latest cfmljure CFC that we use at World Singles is up-to-date in
github but the documentation may be a bit behind.
Note also that interop between ColdFusion and Clojure is nowhere near
as seamless as between Railo and Clojure due to how arrays of numbers
are handled (in ColdFusion they are mostly arrays of strings that
ColdFusion auto converts on demand, which does not work when you pass
it to Clojure / Java; in Railo, they are arrays of numbers - with the
consequent performance improvement as well).
The TL;DR guide to getting cfmljure working with Tomcat (I haven't
tested it with other containers) is:
* Use Leiningen to create / manage your Clojure project (it's a simple
build tool)
* If your Clojure project folder is /path/to/folder, add
/path/to/folder and /path/to/folder/src to common.loader in
{tomcat}/conf/catalina.properties
* After running lein deps (to download / install libraries for your
Clojure project - including Clojure itself), copy
/path/to/folder/lib/*.jar to {web root}/WEB-INF/lib/ - although watch
out for JAR conflicts (e.g., DB drivers, javax.mail...)
* Restart Tomcat to pick up those libraries - only needed when you
update dependencies
In your CFML code, create a new cfmljure CFC and call install(
"list,of,namespaces", somestruct ) to install those namespaces from
Clojure as keys in the struct - I generally install into the this
scope of a CFC which I reference as variables.clj (injected
everywhere). For simple use, you can install( "list,of,namespaces",
variables ) to add the Clojure namespaces to your variables scope.
e.g.,
var clj = new cfmljure();
clj.install( "clojure.core", variables );
var x = variables.clojure.core.map( variables.clojure.core._inc(), [
1, 2, 3, 4 ] );
The ._{name}() notation returns a reference to {name} so it's how you
get clojure.core/inc to pass as the first argument to
clojure.core/map. You could also do:
var x = variables.clojure.core.map( variables.clojure.core._( "inc" ),
[ 1, 2, 3, 4 ] );
._( "{name}" ) is the same as ._{name}()
HTH
On Apr 9, 2012, at 19:23, Sean Corfield <seanco...@gmail.com> wrote:
> On Mon, Apr 9, 2012 at 9:37 AM, Nando <d.n...@gmail.com> wrote:
>> Thanks very much for this comprehensive reply, Sean. It's inspired me to
>> look deeper into cfmljure and clojure.
>
> The latest cfmljure CFC that we use at World Singles is up-to-date in
> github but the documentation may be a bit behind.
Thanks again Sean. Good to know. I assume it's fine to use Clojure 1.3 with cfmljure? I saw a note on Github about reverting to 1.2 to get everything working again, but have the impression that's out of date.
>
> Note also that interop between ColdFusion and Clojure is nowhere near
> as seamless as between Railo and Clojure due to how arrays of numbers
> are handled (in ColdFusion they are mostly arrays of strings that
> ColdFusion auto converts on demand, which does not work when you pass
> it to Clojure / Java; in Railo, they are arrays of numbers - with the
> consequent performance improvement as well).
Planning on using Railo from your previous comments. For the rest, I have some catching up to do. Where is the best place to ask questions if I get stuck?
1.3.0 is what we use at World Singles.
> I saw a note on Github about reverting to 1.2 to get everything working again, but have the impression that's out of date.
It's due to the examples requiring the old contrib library. I need to fix that!
> Planning on using Railo from your previous comments. For the rest, I have some catching up to do. Where is the best place to ask questions if I get stuck?
https://groups.google.com/forum/?fromgroups#!forum/cfmljure (looks
like you tried back in October but ran into the ACF issues as well as
noting the 1.2 compatibility issue).
One thing I'm planning to do is create an "express" bundle of Jetty +
Railo + cfmljure + Clojure libs so folks can just download, expand and
play. Time, just need time!
Not based on what code you've shown.
> <cfset local.ormObject = entityNew(arguments.tableName)>
> <cfset local.ormObject = setORMAttributes(local.ormObject,
> arguments)>
> <cfset entitySave(local.ormObject)>
> <cfset ormFlush()>
Couple of things:
* have you disabled flush at end of session for ORM?
* instead of ormFlush(), wrap operations in cftransaction:
<cftransaction>
entityNew...
...
entitySave...
</cftransaction>
<cftransaction>
entityLoadByPK...
...
entitySave...
</cftransaction>
> My guess (may be totally
> wrong) that ORM object is cached within application scope where FW/1
Totally wrong :)
> Another fear here is that if my guess is correct, session persists and
> doesn't clear, than multiple sessions build up ummm... somewhere until
> server will run out of memory.
Also totally wrong :)
The ORM "session" is actually per-request by default and (essentially)
per-transaction when you take over Hibernate object lifecycle
management yourself.
> http://groups.google.com/group/cfwheels/browse_thread/thread/1e60ff71ab381dd3/03343cf43605dca7
A limitation of cfWheels, not of Hibernate (fortunately).
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
I'd argue that your architecture is flawed there. A single request
should not try to save tens of thousands of anything - unless you're
talking about a background process job? (for which CFML isn't really
suitable since it's inherently a request/response-based system). I'd
also note that Java developers are able to use Hibernate like this -
but of course the overhead of Java objects compared to CFCs is much
less (and it _is_ reasonable to write background process jobs in Java
anyway).
FWIW, one of the nice things about dropping down into Clojure is that
it's easy to dispatch work in the background:
(future (doseq [period period-collection] (save-row :workperiod period)))
"future" says execute this code in another thread and just continue on.
But I agree that a single SQL insert with multiple rows will be more
efficient than "10's of thousands" of individual inserts. Clojure's
JDBC library has a shorthand for that as long as you're willing to
convert your records into arrays of values and provide an array of
column names up front:
(future (jdbc/insert-values :workperiod [:id :name :value ...] [ [1
"first" 42 ] [ 2 "second" 13 ] ... ]))
And even that's easy to do - assuming all your work period records
have all the same keys:
(future
(let [cols (keys (first period-collection))]
(jdbc/insert-values :workperiod cols (map (apply juxt cols)
period-collection))))
"keys" returns the keys of the first record in the array and the keys
are keywords (:foo) so they can be used as functions (taking a record,
returning the matching key's value). "juxt" takes a number of
functions and returns a new function that produces an array by
applying the sequence of functions to its argument - (juxt :id :name
:value) is a function that takes a record and returns an array
containing the ID, the name and the value elements of that record.
"apply" is how you convert a collection to an argument list: (apply
juxt [:id :name :value]) is equivalent to (juxt :id :name :value) so
you can use computed argument lists (like argumentCollection= in
CFML).
If the records in period-collection don't all have the same full set
of keys, then (reduce (fn [ks m] (union ks (set (keys m)))) #{}
period-collection) will give you a full set of the unique keys across
all of those records. Or (apply union (map (comp set keys)
period-collection)) if you prefer :)
Anyways, that's probably more Clojure than anyone wanted in a thread
that started out asking about controllers calling controllers - it was
mostly for Nando's benefit since he's been asking about Clojure.
> and here's how I persist an employee:
I'd probably start the transaction, load/create the entity, populate
and validate it, save it - if valid, and then end the transaction
since to me that's the real extent of the transaction, not just the
save operation. I think dropping down to the underlying Hibernate
session machinery like that is really ugly code - and unnecessary if
you wrap <cftransaction> around code at the appropriate level.
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
No it won't. I've seen lots of code that updates arguments scope variables.
> <cfset local.ormObject = duplicate(arguments.ormObject)>
Calling duplicate() on an object is a pretty dangerous thing to do,
IMO. I preferred the CFMX7 behavior where duplicating a CFC threw an
exception. I was sorry they changed it in CF8. I blogged about it:
http://corfield.org/entry/duplicate_is_bad_for_your_objects_health
(it lost formatting when I imported it into MangoBlog so it's a little
hard to read)
Glad you got it working!
As far as I am aware, you've always been able to assign to arguments.
In fact, before CFMX had a var (local) scope, that was the only thread
safe way to handle local variables: by assigning into arguments scope!
> I can't call a controller from another controller. But I can call a
> service. Services are queued up after controllers, so any variables
> set in a services aren't visible in controller (just because they
> don't exists during controller execution).
They would be visible in the endItem() controller method so use
startItem() / endItem() instead of just item().
Or manage services yourself (either explicitly or via a DI framework)
and call them directly in the item() method. The service queueing
machinery is something most people outgrow fairly quickly.
> Question... Where to put shared validation code?
In a service.
> I want to have a single method for validating
> this field and call it from different parts of the framework.
Yup, that's exactly what services are for.
I'd probably start the transaction, load/create the entity, populate
and validate it, save it - if valid, and then end the transaction
since to me that's the real extent of the transaction, not just the
save operation. I think dropping down to the underlying Hibernate
session machinery like that is really ugly code - and unnecessary if
you wrap <cftransaction> around code at the appropriate level.
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/
"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one
Cool. That's much cleaner code. I'd probably do this tho':
var needToSync = false;
transaction {
if(structkeyexists(rc, 'id')) {
rc.maskItem = getMaskService().getMaskItem(rc.id);
}
if(ListContains(session.permission,"mask.approve")) {
rc.maskItem.setIsActive(2);
rc.maskItem.setIsLocked(1);
rc.maskItem.setIsVirgin(0);
// the transaction is commited in 2 steps so that the mask item has
all parameters set before the sync is run
getMaskService().persistMaskItem(rc.maskItem);
needToSync = true;
}
}
if ( needToSync ) {
transaction {
getMaskService().syncWorkPeriodsToMask(rc.id);
}
}
I don't think your second rollback would rollback the already
committed transaction that saved the maskItem, so I don't think it's
clear to nest it inside the same transaction?
I'd probably also forgo getMaskService().persistMaskItem(rc.maskItem)
and just use entitySave(rc.maskItem) - the service method doesn't do
anything useful so it seems like wasteful delegation to me. The same
may be true of getMaskService().getMaskItem(rc.id)? I tend to avoid
service methods that add no value - and try to keep services for
orchestrating operations across multiple domain objects.
I'd probably also add a convenience method to all my domain objects
(via a base CFC):
function save() {
entitySave(this);
}
so that in my controller, I can just say:
rc.maskItem.save();
That reduces the coupling between the controller and the service - and
empowers the business object.
If mask approval is active = 2 (a magic number - bad!), locked = 1,
virgin = 0, I'd probably put that in a method on the domain object
too:
function approve() {
this.setIsActive( 2 ); // use a symbolic name for this!!
this.setIsLocked( 0 );
this.setIsVirgin( 0 );
}
Then your controller becomes:
var needToSync = false;
transaction {
if(structkeyexists(rc, 'id')) {
rc.maskItem = getMaskService().getMaskItem(rc.id);
// or rc.maskItem = entityLoadByPK( "Mask", rc.id );
// or whatever the ORM syntax is
}
if(ListContains(session.permission,"mask.approve")) {
rc.maskItem.approve();
rc.maskItem.save();
needToSync = true;
}
}
if ( needToSync ) {
transaction {
getMaskService().syncWorkPeriodsToMask(rc.id);
}
}
Potentially no service dependency. No business logic for the
"approval". And a very clear transaction structure.
*grin*
> Thank you so very much! Your advice helps a LOT! I wish I could share
> the same office/cubic with you.
Remember: we're never done learning and the best way to eat an
elephant is one bite at a time :D
I try to live by the motto "learn something new every day" (and
working with Clojure that's pretty much guaranteed at the moment!).
> if(ListContains(session.permission,"mask.approve")) {
Just a quick point: Careful with ListContains! it returns sub-strings
of list elements:
It'll find "approved" in "blah,blah,disapproved,blah"
but listfind() won't.
--
FW/1 on RIAForge: http://fw1.riaforge.org/
FW/1 on github: http://github.com/seancorfield/fw1
FW/1 on Google Groups: http://groups.google.com/group/framework-one