BoxLang 🚀 A New JVM Dynamic Language Learn More...
orm4cf is a community-maintained ORM module for CFML engines, maintained by the modules4cf contributors. It enhances the Hibernate-based CFML ORM with service layers, Active Record, fluent criteria queries, dynamic finders and more.
modules4cf is an independent, engine-neutral CFML community project. It is not a Lucee project and lives outside the Lucee organization.
Status: orm4cf is based on cborm 5.1.0. Lucee support, which cborm 5.x dropped, has been restored. The test suite runs in CI on Lucee 6, Lucee 7, Adobe ColdFusion 2023 and Adobe ColdFusion 2025.
orm4cf is a fork of cborm version 5.1.0 by Ortus Solutions, Corp. The original cborm documentation is at https://coldbox-orm.ortusbooks.com/ and still describes most of the API.
Original copyright: Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp
orm4cf, like cborm, is licensed under the Apache License, Version 2.0. See NOTICE for attribution details.
ColdBox and cborm are trademarks of Ortus Solutions, Corp, used here for attribution only. This project is not affiliated with, sponsored by, or endorsed by Ortus Solutions, Corp.
# A quick preview of some functionality
var book = new Book().findByTitle( "My Awesome Book" );
var book = new Book().getOrFail( 2 );
property name="userService" inject="entityService:User";
return userService.list();
return userService.list( asStream=true );
userService
.newCriteria()
.eq( "name", "luis" )
.isTrue( "isActive" )
.getOrFail();
userService
.newCriteria()
.isTrue( "isActive" )
.joinTo( "role", "r" )
.eq( "r.name", "admin" )
.asStream()
.list();
userService
.newCriteria()
.withProjections( property="id,fname:firstName,lname:lastName,age" )
.isTrue( "isActive" )
.joinTo( "role", "r" )
.eq( "r.name", "admin" )
.asStruct()
.list();
Apache License, Version 2.0. See LICENSE and NOTICE.
Using an AI coding assistant? orm4cf ships a skill
file in the open Agent
Skills format: skills/orm4cf/SKILL.md
. It gives your assistant the correct install steps, settings,
APIs and cborm migration notes, so it doesn't have to guess.
https://raw.githubusercontent.com/modules4cf/orm4cf/main/skills/orm4cf/SKILL.md
https://raw.githubusercontent.com/modules4cf/.github/main/skills/modules4cf/SKILL.md
Point your assistant at the raw URL, or copy the
skills/orm4cf/ folder into your project's or your
assistant's skills folder.
Use the CommandBox CLI to install:
# orm4cf is not published to ForgeBox yet. Until then, install a release from GitHub:
box install modules4cf/orm4cf#v5.2.1
Release zips are also attached to each GitHub
release as orm4cf-<version>.zip.
This installs the module into modules/orm4cf/.
Unfortunately, due to the way that ORM is loaded by ColdFusion, if
you are using the ORM EventHandler or ActiveEntity or any
ColdBox Proxies that require ORM, you must create an Application
Mapping in the Application.cfc like this:
this.mappings[ "/cborm" ] = COLDBOX_APP_ROOT_PATH & "modules/orm4cf";
This is due to the fact that the ORM event listener starts before ColdBox, so no dynamic mappings exist yet. Important: Make sure you ALWAYS lazy load dependencies in your event handlers to avoid chicken and the egg issues.
The module also registers a new WireBox DSL called
entityservice which can produce virtual or base ORM
entity services:
entityservice - Inject a global ORM service so you can
work with ANY entityentityservice:{entityName} - Inject a Virtual entity
service according to entityName
Here are the module settings you can place in your
config/Coldbox.cfc under the
moduleSettings.cborm structure:
moduleSettings = {
cborm = {
// Resource Settings
resources : {
// Enable the ORM Resource Event Loader
eventLoader : false,
// Prefix to use on all the registered pre/post{Entity}{Action} events
eventPrefix : "",
// Pagination max rows
maxRows : 25,
// Pagination max row limit: 0 = no limit
maxRowsLimit : 500
},
// WireBox Injection Bridge
injection : {
enabled : true,
include : "",
exclude : ""
}
}
};
We have also migrated the UniqueValidator from the
validation module into our ORM module. It is mapped
into wirebox as UniqueValidator@cborm so you can use in
your constraints like so:
this.constraints = {
"name" : {
"required":true,
"validator":"UniqueValidator@cborm"
}
};
Note: the module installs into
modules/orm4cf/and is registered in ColdBox asorm4cf(for examplegetModuleSettings( "orm4cf" )). For compatibility the/cbormmapping,cborm.models.*paths,@cbormWireBox IDs andmoduleSettings.cbormstay the same.
All contributions welcome! Feel free to fix a typo, add a feature 🚀 , or add a testbox spec for a newly discovered issue 🐛
If you want to get hacking on orm4cf, here's how to start:
docker-compose.yml file. Just make sure you have Docker
installed. run-script startdbs or run it manually below.docker run \
-e MYSQL_RANDOM_ROOT_PASSWORD=yes \
-e MYSQL_USER=other \
-e MYSQL_PASSWORD=ortussolutions \
-e MYSQL_DATABASE=coolblog \
-v "$PWD/test-harness/tests/resources":/docker-entrypoint-initdb.d \
-p 3306:3306 \
--detach \
--name cborm_mysql \
mysql
.env.template to .env and enter the
database credentials used in step 2 above ☝box
run-script install:dependencies
box server start [email protected]
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
cbpaginator dependency (HTTP 403). cbpaginator 2.8.3 is now installed from its GitHub repository (coldbox-modules/cbpaginator, pinned to the commit of 2.8.3), since that repository has no release tags./cborm application mapping now points to modules/orm4cf, and the joinTo() examples pass the required alias.LICENSE file with the full Apache License, Version 2.0 text.NOTICE file attributing the upstream cborm project (Apache License 2.0, section 4).ActiveEntity method: validate():ValidationResult which will validate the entity and return a ValidationResult object with all the validation results.when(), unless() closures now receive the instance as an argument to the closures.profiles to the validation methods in the ActiveEntity.ignoreTargetLists argument to populatexxx() methods in all services.isValid() needs a this.isValid() to resolve ambiguity with BIF.entityGivenName method of ORMUtilSupport.cfc not returning Hibernate result and producing a null error.PostLoad event handler for multi-datasource entitiesActiveEntity:
peek( closure ) to allow for peeking into the building process. Pass in your closure that receives the entity and interact with it.when( boolean, successClosure, failureClosure ) that you can use to build functional entities without the use of if statements.unless( boolean, successClosure, failureClosure ) that you can use to build functional entities without the use of if statements. The opposite of when().throwIf( boolean, type, [message], [detail] ) that you can use to throw an exception if a condition is met.throwUnless( boolean, type, [message], [detail] ) that you can use to throw an exception if a condition is not met.lazy annotations that conflict with cb7 lazy propertiesprocessState() to announce() on all testsprocessState() to announce() to stay compliantannounceInterception() to announce() to stay compliantwhen( boolean, success, fail ) fluent construct for ActiveEntity, VirtualEntityService and the BaseORMService to allow for fluent chaining of operations on an entity or it's service.countWhere() invalid SQL exception if no arguments are provided: https://github.com/coldbox-modules/cborm/pull/54Subqueries was marked as a singleton when indeed it was indeed a transient. This could have created scoping issues on subquery based detached criteria building.BaseBuilder detached projectionsDetachedCriteriaBuilder was not passing the datasource to native criteria objectsdocker-compose.yml to startup MySQL, or PostgreSQL in docker, for further hacking and testing.buildJavaProxy() which leverages our JavaProxyBuilderevict() had the wrong method and arguments delegated to the parent class.isInTransaction() util helper method to all the orm services.ORMFlush, ORMAutoFlush, ORMPreFlush, ORMDirtyCheck, ORMEvict, and ORMClear? has been deprecated. You will have to use the ?x approach where x is a number according to the position in the sql:// Old Syntax
select p
from Person p
where p.name like ? and p.isStatus = ?
// New Syntax
select p
from Person p
where p.name like ?1 and p.isStatus = ?2
eventPrefix setting so you can prefix the resource REST CRUD events with whatever you like.results struct does not have the required keysvariables.saveMethod property or the savemethod argument.variables.deleteMethod property or the deleteMethod argument.getSQLHelper() from criterias to allow for usage of formmatting of sqlbeforeOrmExecuteQuery, afterOrmExecuteQuery from the base orm service: executeQuery() methodafterCriteriaBuilderList event before results conversionsafterCriteriaBuilderGet, beforeCriteriaBuilderGet called after/before criteria get() callsnew() method instead of being auto-wired after population.processEntityInjection() public on the ORM Event Handler so it can be reused in other locationsprocessEntityInjection() returns the passed entity so you can do chaininggetOrFail() now includes in the extendedInfo the actual entity that caused the exceptionORMUtilSupport when detecting datasources, if you passed a default it would never be usedpostNew was not using the actual entity name so we where hitting performance on lookups for namefalse for resource handler and it needed to be trueFeatures : Introduction of the automatic resource handler for ORM Entities based on ColdBox's 6 resources and RestHandlerImprovement : Natively allow for nested transactions and savepoints by not doing preemptive transaction commits when using transactions.Bug : Fix on getOrFail() where if the id was 0, it would still return an empty object.Task : Added formatting via cfformatFeature : Upgraded to cbValidation 2.0.0Feature : Updated the unique validator to match 2.0.0 standardsFeature : Upgraded to mementifier 2.0.0improvement : In executeQuery() Determine if we are in a UPDATE, INSERT or DELETE, if we do, just return the results instead of a stream or query as the result is always numeric, the rows that were altered.bug : Fixed asStream typo on executeQuery()bug : Missing ACF2016 compat on testsbug : virtual entity service still had entity required for casting methodsFeature: New function for criteria query when( boolean, target ) that you can use to build functional criterias without the use of if statements.
newCriteria() .when( isBoolean( arguments.isPublished ), function( c ){ // Published bit c.isEq( "isPublished", isPublished ); // Published eq true evaluate other params if( isPublished ){ c.isLt( "publishedDate", now() ) .$or( c.restrictions.isNull( "expireDate" ), c.restrictions.isGT( "expireDate", now() ) ) .isEq( "passwordProtection","" ); } } ) .when( !isNull( arguments.showInSearch ), function( criteria ){ c.isEq( "showInSearch", showInSearch ); } ) .list()
Feature: Missing nullValue() is BaseBuilder class
Feature: Added new criteria query peek( closure ) function to allow for peeking into the building process. Pass in your closure that receives the criteria and interact with it.
Feature: Added a validateOrFail() to the active entity, which if the validation fails it will throw an exception or return back to you the same entity validated now.
Improvement: Better documentation for deleteById() since it does bulk deletion, which does not do any type of cascading.
Improvement: isValid() in active entity missing includeFields argument
Improvement: Timeout hints for criteria builder
Improvement: Updated exception type for criteria builder get()
Bug: ACF2016 issues with elvis operator.
Bug: getOrFail() had an invalid throw statement
populate() in ActiveEntity so the target is the last argument so you can just pass a struct as the first argument #29save() operation return the saved entity or array of entities instead of the BaseORM service #28orm configuration structure in your config/ColdBox.cfc to the moduleSettings struct and rename it to cborm to standardize it to module settings.moduleSettings = {
cborm = {
inject = {
enabled = true,
includes = "",
excludes = ""
}
}
};
deleteByQuery() reworked entirely to do native bulk delete queries. It now also returns the number of records removedevict() method was renamed to evictCollection() to better satisfy the same contract in hibernateevictEntity() method was renamed to evict() to better satisfay the same contract in hibernatebyExample on many listing methodsget() -> getOrFail() to throw an entity not found exceptionasStruct(), asStream(), asDistinct() that will apply result transformers for you instead of doing .resultTransformer( c.ALIAS_TO_ENTITY_MAP ), whish is long and boring, or return to you a java stream via cbStreams.not. So you can do: .notEq(), notBetween(), notIsNull(), notIsIn() and much more.list() method has a new asStream boolean argument that if true, will return the results as a cbStream. ((www.forgebox.io/view/cbStreams))idCast() and autoCast() added for quick casting of valuesqueryHint() so you can add your own vendor specific query hints for optimizers.comment( string ) so you can add arbitrary comments to the generated SQL, great for debuggingsqlRestriction() deprecated in favor of the shorthand notation: sql()sql() restriction now supports binding positional parameters. You can pass them in an array and we will infer the types: sql( "id = ? and isActive = ?", [ "123", true ] ). Or you can pass in a struct of {value:"", type:""} instead:restrictions.sql( "userName = ? and firstName like ?", [
{ value : "joe", type : "string" },
{ value : "%joe%", type : "string" }
] );
The available types are the following which match the Hibernate Types
this.TYPES = {
"string" : "StringType",
"clob" : "ClobType",
"text" : "TextType",
"char" : "ChareacterType",
"boolean" : "BooleanType",
"yesno" : "YesNoType",
"truefalse" : "TrueFalseType",
"byte" : "ByteType",
"short" : "ShortType",
"integer" : "IntegerType",
"long" : "LongType",
"float" : "FloatType",
"double" : "DoubleType",
"bigInteger" : "BigIntegerType",
"bigDecimal" : "BigDecimalType",
"timestamp" : "TimestampType",
"time" : "TimeType",
"date" : "DateType",
"calendar" : "CalendarType",
"currency" : "CurrencyType",
"locale" : "LocaleType",
"timezone" : "TimeZoneType",
"url" : "UrlType",
"class" : "ClassType",
"blob" : "BlobType",
"binary" : "BinaryType",
"uuid" : "UUIDCharType",
"serializable" : "SerializableType"
};
maxResults( maxResults ) method to limit the results bygetOrFail() proxies to get(), findOrFail() proxies to findIt() that if not entity is produced will throw a EntityNotFound exceptionasStream boolean argument.criteriaCount(), criteriaQuery() from BaseService, this was the legacy criteria builder approach, please use newCriteria() instead.getEntityGivenName to support ACF2018BeanPopulator for performance on creationsORMEventHandler for performance on creationsrestrictions for performance on creationsdatasource, or uses the default one declareddatasource to many listing methodsignoreCase, sorting and timeouts.getAll() to retrieve read only entities using the readOnly argument.getAll() method has a new properties argument that if passed will allow you to retrieve an array of structs according to the passed in properties.idCast( entity, id ) to auto cast your entity id value to java type automatically for you, no more javacastingautoCast( entity, propertyName, value ) to auto cast any value for any entity property automatically, no more javacasting.getKeyValue( entity ) which will give you the value of the entity's unique identifierisDirty( entity ) which will let you know if the entity has dirty values or has its values changed since loaded from the dbgetEntityMetadata( entity ) which will return to you the hibernate's metadata for a specific entity.getPropertyNames() argument of entityname renamed to entity to allow not only for a name but an actual entity as well.getTableName() argument of entityname renamed to entity to allow not only for a name but an actual entity as well.getKey() argument of entityname renamed to entity to allow not only for a name but an actual entity as well.getEntityMetadata()deleteByQuery() reworked entirely to do native bulk delete queries. It now also returns the number of records removeddeleteWhere() missing flush argument, added datasource as wellwirebox : a WireBox reference already injected, logger : a prepared logger for the class, datasource The default datasource or constructed datasource for the class.debug level, even for dynamic methods.options that can have the following keys now:{
ignoreCase : boolean (false)
maxResults : numeric (0)
offset : numeric (0)
cacheable : boolean (false)
cacheName : string (default)
timeout : numeric (0)
datasource : string (defaults)
sortBy : hql to sort by,
autoCast : boolean (true),
asStream : boolean (false)
}
results = ormservice.findByLastLoginBetween( "User", "01/01/2008", "11/01/2008", { sortBy="LastName" } );
autocast:false in the options to the calls.#Remember this entity extends Base Service, so we get all the features above plus the following:
Remember this entity extends the Virtual Service, so we get all the features above plus the following:
refresh(), merge(), evict() refactored to encapsulate login in the base orm service and not itselfgetKey() return typing to allow composite keys: https://github.com/coldbox-modules/cborm/pull/21cbvalidation to v1.1.0StringBuffer with StringBuilder for performance
$
box install orm4cf