diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2374642f..95ba8a41 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Java uses: actions/setup-java@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf6037e6..eee52007 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: issues: write steps: - name: Checkout Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup CommandBox uses: Ortus-Solutions/setup-commandbox@v2.0.1 @@ -115,7 +115,7 @@ jobs: - name: Upload Build Artifacts if: success() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: boxlang-build path: | @@ -188,12 +188,12 @@ jobs: issues: write steps: - name: Checkout Development Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: development - name: Download build artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: name: boxlang-build path: .tmp diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 935782bf..6da34b36 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -37,7 +37,7 @@ jobs: contents: write checks: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Setup Java uses: actions/setup-java@v5 @@ -50,7 +50,7 @@ jobs: ./gradlew spotlessApply --stacktrace - name: Commit Format Changes - uses: stefanzweifel/git-auto-commit-action@v6 + uses: stefanzweifel/git-auto-commit-action@v7 with: commit_message: Apply cfformat changes diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89b3b1a7..90ce74f2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ on: env: MODULE_ID: ${{ github.event.repository.name }} - GRADLE_VERSION: 8.7 + GRADLE_VERSION: 8.14.1 jobs: tests: @@ -35,7 +35,7 @@ jobs: # experimental: true steps: - name: Checkout Repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Java uses: actions/setup-java@v5 @@ -78,7 +78,7 @@ jobs: - name: Upload Test Results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: tests-${{ matrix.os }}-${{ matrix.jdkVersion }} path: | @@ -110,7 +110,7 @@ jobs: issues: read steps: - name: Download Artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: path: artifacts diff --git a/.gitignore b/.gitignore index f8a75d38..4715dbb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle -build/** +build/ bin/ +test/ !gradle/wrapper/gradle-wrapper.jar # Secrets @@ -23,7 +24,8 @@ derby.log src/test/resources/libs/* !src/test/resources/libs/.gitkeep src/test/resources/modules/ -grapher/** +grapher/ +boxlang_modules/ ## HBXML Files src/test/resources/app/models/**/*.hbm* diff --git a/COPILOT_INSTRUCTIONS.md b/COPILOT_INSTRUCTIONS.md new file mode 100644 index 00000000..47779b17 --- /dev/null +++ b/COPILOT_INSTRUCTIONS.md @@ -0,0 +1,44 @@ +# Copilot Instruction File for bx-orm + +## High-Level Purpose + +The `bx-orm` module provides Object-Relational Mapping (ORM) capabilities for the BoxLang and boxlang JVM language. It enables developers to map BoxLang objects to relational database tables, manage database schema, and perform CRUD operations using a high-level, object-oriented approach. The module abstracts database interactions, allowing for more maintainable and database-agnostic application code. + +bx-orm sits as a middleware between the boxlang dynamic JVM language and Hibernate ORM. It abstracts not only database operations, but the verbose Hibernate configuration syntax. + +Due to issues with JPA requiring native java classes in entity configuration, bx-orm utilizes Hibernate 5.6.15-FINAL which enables dynamic java classes in place of Java source files. Hence all Hibernate integration code is written against Hibernate 5, not Hibernate 6 or 7. + +## Module Structure and Design + +- src/main/bx/**: Contains BoxLang source files, including the module settings file `ModuleConfig.bx` and boxlang interfaces for event handling, naming strategies, etc. +- src/main/java/**: Java implementation of the ORM engine + - src/main/java/ortus/boxlang/modules/orm/bifs/**: Built-in functions for interacting with ORM entities or the ORM session. + - src/main/java/ortus/boxlang/modules/orm/config/**: Hibernate configuration-related files, such as the event handler, connection provider, naming strategies, and base Hibernate configuration wrapper. + - src/main/java/ortus/boxlang/modules/orm/hibernate/**: Houses hibernate interface implemenations for value casters (converters), the hibernate cache, and especially box class to hibernate entity proxy objects. + - src/main/java/ortus/boxlang/modules/orm/mapping/**: Houses classes that assist in parsing boxlang ORM entities into ORM context/state and generating hibernate HBM.xml configuration files +- src/main/test/java/ortus/**: Junit tests +- src/main/test/java/ortus/tools/**: Base test files and util classes for assistance in writing junit tests +- src/main/test/resources/app/**: Test boxlang files for a test app. Includes ORM models, ORM configuration in Application.bx, and other boxlang test files. +- src/main/resources/**: Resource files such as configuration, metadata, and licensing. +- build/**: Build artifacts, generated sources, and documentation. +- bin/**: Packaged module binaries and metadata for distribution. + +## Design Principles + +- **Separation of Concerns**: Java code handles low-level ORM logic, while BoxLang code provides configuration and high-level integration. +- **Extensibility**: The module supports custom naming strategies, event handlers, and database dialects. +- **Testability**: Includes comprehensive test cases and seed data to ensure reliability across different environments. +- **Documentation**: Extensive documentation and examples are provided to help users understand and extend the module. + +## Usage Guidance for Copilot +- Follow the established directory structure when adding new features. +- Prefer extending existing interfaces and base classes for new ORM features. +- Ensure new code is covered by tests in the `src/main/test/java/ortus/` directory. +- Ensure new features, bug fixes, security updates, etc. are added to `changelog.md` under `## [Unreleased]`. + +## Tooling + +* Gradle is used for building/compiling the java sources, running junit tests, and building the final boxlang module structure into a zip file for uploading to forgebox.io. +* Hibernate 5.6.15-FINAL serves as the ORM engine under the hood. +* Spotless is used for java source formatting. +* Docker-compose is used to stand up a simple mysql database for integration testing. \ No newline at end of file diff --git a/build.gradle b/build.gradle index eb82bf13..ecf198e0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,11 +2,11 @@ plugins { id 'java' // For source code formatting - id "com.diffplug.spotless" version "7.2.1" + id "com.diffplug.spotless" version "8.1.0" // https://github.com/harbby/gradle-serviceloader id "com.github.harbby.gradle.serviceloader" version "1.1.9" // Shadow - id "com.gradleup.shadow" version "9.0.0-beta17" + id "com.gradleup.shadow" version "9.2.2" // Download task id "de.undercouch.download" version "5.6.0" } @@ -42,8 +42,8 @@ dependencies { compileOnly files( '../../boxlang/build/libs/boxlang-' + boxlangVersion + '.jar' ) testImplementation files( '../../boxlang/build/libs/boxlang-' + boxlangVersion + '.jar' ) } else { - compileOnly "io.boxlang:boxlang:1.6.0" - testImplementation "io.boxlang:boxlang:1.6.0" + compileOnly "io.boxlang:boxlang:1.7.0" + testImplementation "io.boxlang:boxlang:1.7.0" } // Implementation Dependencies @@ -54,11 +54,11 @@ dependencies { // https://mvnrepository.com/artifact/javax.cache/cache-api implementation 'javax.cache:cache-api:1.1.1' // https://mvnrepository.com/artifact/org.ehcache/ehcache - implementation 'org.ehcache:ehcache:3.10.8' + implementation 'org.ehcache:ehcache:3.11.1' // Testing Dependencies // Pinning this because the next version up breaks our tests - testImplementation "org.junit.jupiter:junit-jupiter:5.+" + testImplementation "org.junit.jupiter:junit-jupiter:6.+" testImplementation "org.mockito:mockito-core:5.+" testImplementation "com.google.truth:truth:1.+" // Explicitly declare the JUnit platform launcher (to avoid deprecation) @@ -152,7 +152,6 @@ shadowJar { mergeServiceFiles() destinationDirectory = file( "build/libs" ) } -build.finalizedBy( shadowJar ) /** * Source Code Formatting @@ -174,6 +173,7 @@ spotless { * - Copies the src/main/bx/** to build/module/ folder. */ task createModuleStructure(type: Copy) { + mustRunAfter shadowJar, jar from( 'build/libs' ) { include "${project.name}-${version}.jar" into 'libs' @@ -181,12 +181,12 @@ task createModuleStructure(type: Copy) { from( 'src/main/bx' ) { include '**/*.bx' // Token Replacements Go Here - filter { line -> line.replaceAll('@build.version@', project.version) } filter { line -> line.replaceAll('@build.hibernateVersion@', hibernateVersion) } + filter{ line -> line.replaceAll( '@build.version@', project.version ) } if( project.branch == "development" ){ - filter { line -> line.replaceAll( '\\+@build.number@', '' ) } + filter{ line -> line.replaceAll( '\\+@build.number@', '' ) } } else { - filter { line -> line.replaceAll( '@build.number@', project.buildID ) } + filter{ line -> line.replaceAll( '@build.number@', project.buildID ) } } } from( '.' ) { @@ -194,11 +194,11 @@ task createModuleStructure(type: Copy) { include 'readme.md' include 'changelog.md' // Token Replacements Go Here - filter { line -> line.replaceAll( '@build.version@', project.version ) } + filter{ line -> line.replaceAll( '@build.version@', project.version ) } if( project.branch == "development" ){ - filter { line -> line.replaceAll( '\\+@build.number@', '' ) } + filter{ line -> line.replaceAll( '\\+@build.number@', '' ) } } else { - filter { line -> line.replaceAll( '@build.number@', project.buildID ) } + filter{ line -> line.replaceAll( '@build.number@', project.buildID ) } } } diff --git a/changelog.md b/changelog.md index 30e2f7aa..68414569 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [BLMODULES-102](https://ortussolutions.atlassian.net/browse/BLMODULES-102) - Fix ORM usage in threads causing ConcurrentModificationException + ## [1.1.3] - 2025-11-04 ### 🐛 Fixed diff --git a/docs/Configuration.md b/docs/Configuration.md deleted file mode 100644 index c18ef451..00000000 --- a/docs/Configuration.md +++ /dev/null @@ -1,37 +0,0 @@ -# Configuration - -To point Boxlang ORM to your entity class files, use the `entityPaths` setting: - -```js -// Application.bx -this.ormSettings = { - entityPaths: ["models/"], -}; -``` - -## CFML Compatibility - -The following ORM settings have been renamed: - -* `cfcLocation` -> `entityPaths` -* `skipCFCWithError` -> `ignoreParseErrors` - -The following CFML interface names/paths have been changed as well: - -* `CFIDE.orm.IEventHandler` -> `orm.models.IEventHandler` -* `lucee.runtime.orm.naming.NamingStrategy` -> `orm.models.INamingStrategy`. - -And, the following setting defaults are changed in BoxLang from the traditional CFML defaults: - -* `flushAtRequestEnd` - Defaults to `true` in ACF and Lucee, defaults to `false` in BoxLang -* `autoManageSession` - Defaults to `true` in ACF and Lucee, defaults to `false` in BoxLang -* `skipCFCWithError` - Defaults to `true` in ACF and Lucee. In BoxLang, this is implemented as `ignoreParseErrors`, which defaults to `false`. - -To support these legacy setting names and/or default settings, install the [bx-compat-cfml](https://forgebox.io/view/bx-compat-cfml) module. - -## NEW Settings - -The following ORM settings are brand-new for BoxLang and do not exist in either Adobe ColdFusion or Lucee Server: - -* `enableThreadedMapping` - Enable the use of threading to process ORM entities in parallel, greatly speeding up an ORM load. Default `true`. -* `quoteIdentifiers` - Enable quoted identifiers (table names, column names, etc.) to avoid erroring on reserved words. Default `true`. \ No newline at end of file diff --git a/docs/Events.md b/docs/Events.md deleted file mode 100644 index 9336099b..00000000 --- a/docs/Events.md +++ /dev/null @@ -1,98 +0,0 @@ -# ORM Events - -There are several types of event listeners in BoxLang ORM: - -- [ORM Events](#orm-events) - - [Global Event Handler](#global-event-handler) - - [Entity-Specific Event Listeners](#entity-specific-event-listeners) - - [BoxLang Interception Points](#boxlang-interception-points) - -## Global Event Handler - -```js -// Application.bx -this.ormSettings = { - eventHandling : true, - eventHandler : "models.GlobalEventHandler" -} -``` - -Then your event handler will look something like this, implementing any events it wishes: - -```js -// models/GlobalEventHandler.bx -class{ - - function init(){} - - // these events will ONLY fire upon the global event handler - function onEvict(){} - function onDirtyCheck(){} - function onClear(){} - function onAutoFlush(){} - function onFlush(){} - - // These events will fire upon the entity as well - function preLoad(){} - function postLoad(){} - - function preInsert(){} - function postInsert(){} - - function preUpdate(){} - function postUpdate(){} - - function preDelete(){} - function postDelete(){} -} -``` - -## Entity-Specific Event Listeners - -BoxLang ORM will fire certain events on the entity itself if the event listener methods are defined in the entity. - -```js -//models/orm/MyEntity.bx -class{ - // ORM properties here... - - function init(){} - - // Event listener methods here... - function preLoad(){} - function postLoad(){} - - function preInsert(){} - function postInsert(){} - - function preUpdate(){} - function postUpdate(){} - - function preDelete(){} - function postDelete(){} -} -``` - -## BoxLang Interception Points - -These interception points can be used for advanced, deep integration with bx-orm: - -* ORMPreConfigLoad - Allows modification of ORM configuration immediately *before* the configuration is parsed. -* ORMPreConfigLoad - Allows modification of ORM configuration immediately *after* the configuration is parsed. - -Coming soon: - -* `on_save` -* `on_evict` -* `on_dirtyCheck` -* `on_clear` -* `on_auto_flush` -* `on_flush` -* `pre_new` -* `post_new` -* `pre_load` -* `post_load` -* `pre_insert` -* `post_insert` -* `pre_update` -* `post_update` \ No newline at end of file diff --git a/docs/Mapping.md b/docs/Mapping.md deleted file mode 100644 index 4e57efbc..00000000 --- a/docs/Mapping.md +++ /dev/null @@ -1,5 +0,0 @@ -# Defining Entity Mappings - -## Compatibility - -TODO: Fill out this section. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 92ee77cd..00000000 --- a/docs/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# BoxLang ORM - -## Table Of Contents - -* [Configuration](./Configuration.md) -* [Entity Mapping](./Mapping.md) - -## Compatibility - -BoxLang ORM strives to be 100% backwards-compatible with the Adobe ColdFusion and Lucee Server Hibernate ORM implementations. With that said, several older syntaxes and setting names have been changed (with backwards-compatible aliases) for further improvements. - -For more details, see: - -* [Configuration Compatibility](./Configuration.md#compatibility) -* [Entity Mapping Compatibility](./Mapping.md#compatibility) -* [BIF Compatibility](./bifs/README.md#compatibility) \ No newline at end of file diff --git a/docs/bifs/README.md b/docs/bifs/README.md deleted file mode 100644 index 96a8a96c..00000000 --- a/docs/bifs/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# ORM BIFs - -## Compatibility - -* `entityReload()` returns the entity for chainability - `myEntity = entityReload( cachedEntity );` - -### NEW Functionality - -* `entityNameArray()` supports an optional `datasource` argument to filter the result by datasource -* `entityNameList()` supports an optional `datasource` argument to filter the result by datasource \ No newline at end of file diff --git a/docs/cli.md b/docs/cli.md deleted file mode 100644 index a09d510e..00000000 --- a/docs/cli.md +++ /dev/null @@ -1,29 +0,0 @@ -# Entity Mapping CLI - -Test your entity mapping capabilities via this command: - -```sh -java -cp /path/to/bx-orm-1.0.0-all.jar:/path/to/boxlang-1.0.0-snapshot-all.jar \ - ortus.boxlang.modules.orm.cli.GenerateMappings \ - --path /my/app/models/orm -``` - -Where `/path/to/bx-orm-1.0.0-all.jar` is the path to the compiled bx-orm .jar file, and `/path/to/boxlang-1.0.0-snapshot-all.jar` is the path to the compiled boxlang .jar file. - -Options: - -* `--path MY_PATH` - Required. Relative path to the ORM entity files. -* `--failFast` - Inverse of the legacy CFML configuration `skipCFCWithError`. If `--failFast` is passed, the .xml generation will abort if any entity class files fail to parse. -* `--mapping foo:/path/to/foo` - Specify a mapping to a module or other location. This may be necessar for resolving `extends=` values if you are extending a class from another directory. -* `--debug` - Spit out debug logging. Not necessary for your first run, but this may be helpful in debugging mapping errors. - -Here's a full example: - -``` -java -cp /path/to/bx-orm-1.0.0-all.jar:/path/to/boxlang-1.0.0-snapshot-all.jar \ - ortus.boxlang.modules.orm.cli.GenerateMappings \ - --mapping MYMODULE:./modules/MYMODULE/ \ - --mapping cborm:./modules/cborm \ - --path ./models/orm \ - --debug -``` diff --git a/gradle.properties b/gradle.properties index d6bd1873..62d0c96c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ -#Sat Sep 06 17:06:45 UTC 2025 +#Tue Nov 04 18:06:22 UTC 2025 hibernateVersion=5.6.15.Final -boxlangVersion=1.5.0 +boxlangVersion=1.8.0 jdkVersion=21 -version=1.1.3 +version=1.2.0 mysqlVersion=1.0.0 group=ortus.boxlang diff --git a/src/main/java/ortus/boxlang/modules/orm/HQLQuery.java b/src/main/java/ortus/boxlang/modules/orm/HQLQuery.java index 2955f799..bc84bd7d 100644 --- a/src/main/java/ortus/boxlang/modules/orm/HQLQuery.java +++ b/src/main/java/ortus/boxlang/modules/orm/HQLQuery.java @@ -28,6 +28,7 @@ import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.dynamic.casters.CastAttempt; @@ -54,7 +55,7 @@ public class HQLQuery { private Session session; private ORMApp ormApp; private IBoxContext context; - private ORMRequestContext ormRequestContext; + private ORMContext ormContext; private List parameters; private int parameterCount; @@ -66,17 +67,17 @@ public class HQLQuery { private static final String DELETE_PREFIX = "DELETE"; public HQLQuery( IBoxContext context, String hql, Object bindings, IStruct options ) { - this.options = options; - this.context = context; - this.hql = hql; + this.options = options; + this.context = context.getParentOfType( IJDBCCapableContext.class ); + this.hql = hql; - this.ormApp = ormService.getORMAppByContext( context.getRequestContext() ); - this.ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); - this.datasource = options.containsKey( Key.datasource ) ? Key.of( options.getAsString( Key.datasource ) ) : null; - this.session = ormRequestContext.getSession( datasource ); + this.ormApp = ormService.getORMAppByContext( this.context ); + this.ormContext = ORMContext.getForContext( this.context ); + this.datasource = options.containsKey( Key.datasource ) ? Key.of( options.getAsString( Key.datasource ) ) : null; + this.session = ormContext.getSession( datasource ); - this.parameterCount = 0; - this.parameters = processBindings( bindings ); + this.parameterCount = 0; + this.parameters = processBindings( bindings ); } private List processBindings( Object bindings ) { diff --git a/src/main/java/ortus/boxlang/modules/orm/ORMApp.java b/src/main/java/ortus/boxlang/modules/orm/ORMApp.java index 3d53a784..215d3eae 100644 --- a/src/main/java/ortus/boxlang/modules/orm/ORMApp.java +++ b/src/main/java/ortus/boxlang/modules/orm/ORMApp.java @@ -252,14 +252,14 @@ public EntityRecord lookupEntity( String entityName, Boolean fail ) { /** * Load an entity by its primary key. * - * @param context Boxlang Request context + * @param context Boxlang JDBC context * @param entityName The name of the entity to load * @param keyValue The primary key value to load the entity by. This can be a single value such as a string or integer, or a struct for composite * keys. */ - public IClassRunnable loadEntityById( RequestBoxContext context, String entityName, Object keyValue ) { + public IClassRunnable loadEntityById( IBoxContext context, String entityName, Object keyValue ) { EntityRecord entityRecord = this.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context ).getSession( entityRecord.getDatasource() ); + Session session = ORMContext.getForContext( context ).getSession( entityRecord.getDatasource() ); // @TODO: Support composite keys. String keyType = getKeyJavaType( session, entityName ).getSimpleName(); @@ -275,14 +275,14 @@ public IClassRunnable loadEntityById( RequestBoxContext context, String entityNa /** * Load an array of entities by filter criteria. * - * @param context Context in which the BIF was invoked. + * @param context JDBC-capable context in which the BIF was invoked. * @param entityName The name of the entity to load. * @param filter Struct of filter criteria. * @param options Struct of options, including maxResults, offset, order, etc. */ - public Array loadEntitiesByFilter( RequestBoxContext context, String entityName, IStruct filter, IStruct options ) { + public Array loadEntitiesByFilter( IBoxContext context, String entityName, IStruct filter, IStruct options ) { EntityRecord entityRecord = this.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context ).getSession( entityRecord.getDatasource() ); + Session session = ORMContext.getForContext( context ).getSession( entityRecord.getDatasource() ); org.hibernate.Criteria criteria = session.createCriteria( entityRecord.getEntityName() ); if ( filter != null ) { @@ -299,16 +299,20 @@ public Array loadEntitiesByFilter( RequestBoxContext context, String entityName, "No persistent filter property found with the name of '" + key.getName() + "' in entity '" + entityName + "'" ); } ); - filter.entrySet().stream() - .forEach( entry -> { - int propertyIndex = properties.indexOf( KeyCaster.cast( entry.getKey() ) ); - criteria.add( - org.hibernate.criterion.Restrictions.eq( + for ( Key entryKey : filter.keySet() ) { + int propertyIndex = properties.indexOf( KeyCaster.cast( entryKey ) ); + Object propertyValue = filter.get( entryKey ); + criteria.add( + propertyValue != null + ? org.hibernate.criterion.Restrictions.eq( KeyCaster.cast( properties.get( propertyIndex ) ).getName(), - entry.getValue() + propertyValue ) - ); - } ); + : org.hibernate.criterion.Restrictions.isNull( + KeyCaster.cast( properties.get( propertyIndex ) ).getName() + ) + ); + } } return Array.of( diff --git a/src/main/java/ortus/boxlang/modules/orm/ORMRequestContext.java b/src/main/java/ortus/boxlang/modules/orm/ORMContext.java similarity index 74% rename from src/main/java/ortus/boxlang/modules/orm/ORMRequestContext.java rename to src/main/java/ortus/boxlang/modules/orm/ORMContext.java index c9fe1889..2e4c88f7 100644 --- a/src/main/java/ortus/boxlang/modules/orm/ORMRequestContext.java +++ b/src/main/java/ortus/boxlang/modules/orm/ORMContext.java @@ -28,7 +28,6 @@ import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.IJDBCCapableContext; -import ortus.boxlang.runtime.context.RequestBoxContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.jdbc.ConnectionManager; import ortus.boxlang.runtime.jdbc.DataSource; @@ -38,13 +37,24 @@ import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; /** - * Transient context for ORM requests. + * Transient ORM state tracker; manages ORM state for the lifetime of a BoxLang request or thread context. *

- * Tracks Hibernate sessions and transactions for the lifetime of a single Boxlang request. + * Say you call `entityNew()` in a request context, then call it inside a thread loop: + * + * entityNew( "MyEntity" ); // Request context + * items.each( item -> { + * entityNew( "MyEntity" ); // Thread context + * }, true ); // parallel execution + * + *

+ * You now have N+1 Hibernate sessions open (one for the request context, and one for each item in the `items` array). Each of these Hibernate + * sessions is stored in its own ORMContext instance, which is attached to the request or thread context. Each thread will shut down the `ORMContext` + * upon thread completion, which will close all Hibernate sessions opened as part that thread's execution. + * The request context's `ORMContext` will be torn down at the end of the request, closing any remaining Hibernate sessions. * * @since 1.0.0 */ -public class ORMRequestContext { +public class ORMContext { /** * Runtime @@ -63,7 +73,7 @@ public class ORMRequestContext { private ORMApp ormApp; - private RequestBoxContext context; + private IBoxContext context; private ORMConfig config; @@ -73,31 +83,33 @@ public class ORMRequestContext { private Map sessions = new ConcurrentHashMap<>(); /** - * Retrieve the ORMRequestContext for the given context. + * Retrieve the ORMContext for the given boxlang context (whatever JDBC-capable context inside which we are currently executing). * - * @param context The context to retrieve the ORMRequestContext for. + * @param context The context for which to retrieve the ORMContext. * - * @return The ORMRequestContext for the given context. + * @return The ORMContext for the given context. */ - public static ORMRequestContext getForContext( IBoxContext context ) { + public static ORMContext getForContext( IBoxContext context ) { if ( context == null ) { throw new BoxRuntimeException( "Could not acquire ORM context; context is null." ); } - RequestBoxContext requestContext = context.getRequestContext(); - if ( requestContext == null ) { - throw new BoxRuntimeException( "Could not acquire ORM context; supplied context has no parent context which is a request typed." ); + IBoxContext jdbcCapableContext = context.getParentOfType( IJDBCCapableContext.class ); + if ( jdbcCapableContext == null ) { + throw new BoxRuntimeException( "Could not acquire ORM context; supplied context has no parent context which is request or thread typed." ); } - IStruct appSettings = ( IStruct ) requestContext.getConfigItem( Key.applicationSettings ); + // Fix for "effectively final" lambda capture + // https://www.baeldung.com/java-lambda-effectively-final-local-variables + final IBoxContext finalJDBCContext = jdbcCapableContext; + final IStruct appSettings = ( IStruct ) finalJDBCContext.getConfigItem( Key.applicationSettings ); if ( !BooleanCaster.cast( appSettings.getOrDefault( ORMKeys.ORMEnabled, false ) ) ) { throw new BoxRuntimeException( "Could not acquire ORM context; ORMEnabled is false or not specified. Is this application ORM-enabled?" ); } - return requestContext.computeAttachmentIfAbsent( ORMKeys.ORMRequestContext, key -> { - // logger.debug( "Initializing ORM context" ); - return new ORMRequestContext( - requestContext, - new ORMConfig( appSettings.getAsStruct( ORMKeys.ORMSettings ), requestContext ) + return jdbcCapableContext.computeAttachmentIfAbsent( ORMKeys.ORMContext, key -> { + return new ORMContext( + finalJDBCContext, + new ORMConfig( appSettings.getAsStruct( ORMKeys.ORMSettings ), finalJDBCContext ) ); } ); } @@ -105,15 +117,16 @@ public static ORMRequestContext getForContext( IBoxContext context ) { /** * Constructor. * - * @param context The request context. + * @param context The JDBC-capable context (request or thread). * @param config The ORM configuration. */ - public ORMRequestContext( RequestBoxContext context, ORMConfig config ) { + public ORMContext( IBoxContext context, ORMConfig config ) { this.context = context; this.config = config; this.ormService = ( ORMService ) runtime.getGlobalService( ORMKeys.ORMService ); this.ormApp = this.ormService.getORMAppByContext( context ); this.logger = runtime.getLoggingService().getLogger( "orm" ); + this.logger.debug( "Initializing ORM context on context type: {}", context.getClass().getSimpleName() ); } /** @@ -189,7 +202,7 @@ public ORMConfig getConfig() { *

* Will close all Hibernate sessions and unregister the transaction manager. */ - public ORMRequestContext shutdown() { + public ORMContext shutdown() { // Auto-flush all sessions at the end of the request if ( this.config.flushAtRequestEnd && this.config.autoManageSession ) { logger.debug( "'flushAtRequestEnd' is enabled; Flushing all ORM sessions for this request" ); @@ -208,7 +221,7 @@ public ORMRequestContext shutdown() { *

* Attempts a transaction commit prior to closing the sessions, if an active transaction is present. */ - public ORMRequestContext closeAllSessions() { + public ORMContext closeAllSessions() { this.sessions.forEach( ( key, session ) -> { logger.debug( "Closing session on datasource {}", key ); try { @@ -226,7 +239,7 @@ public ORMRequestContext closeAllSessions() { /** * Close the Hibernate session on the given datasource, and ensure it is removed from the session map. */ - public ORMRequestContext closeSession( Key datasourceName ) { + public ORMContext closeSession( Key datasourceName ) { Session session = null; if ( datasourceName == null ) { session = getSession(); @@ -244,7 +257,7 @@ public ORMRequestContext closeSession( Key datasourceName ) { *

* Attempts a transaction commit prior to closing the session, if an active transaction is present. */ - private ORMRequestContext closeSessionAndTransaction( Session session ) { + private ORMContext closeSessionAndTransaction( Session session ) { var tx = session.getTransaction(); if ( tx.isActive() ) { logger.warn( "Session has an active transaction; committing before flushing" ); diff --git a/src/main/java/ortus/boxlang/modules/orm/ORMService.java b/src/main/java/ortus/boxlang/modules/orm/ORMService.java index 139fbc61..3116f4cb 100644 --- a/src/main/java/ortus/boxlang/modules/orm/ORMService.java +++ b/src/main/java/ortus/boxlang/modules/orm/ORMService.java @@ -35,6 +35,7 @@ import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.application.BaseApplicationListener; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.context.RequestBoxContext; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.logging.BoxLangLogger; @@ -305,12 +306,13 @@ public static Object getEntityIdentifier( IClassRunnable entity ) { * @return The primary key value for the given entity instance. */ public static Object getEntityIdentifier( IClassRunnable entity, IBoxContext context ) { - RequestBoxContext requestContext = context.getRequestContext(); - ORMApp ormApp = ORMRequestContext.getForContext( requestContext ).getORMApp(); - String entityName = getEntityName( entity ); - EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context ).getSession( entityRecord.getDatasource() ); - ClassMetadata metadata = session.getSessionFactory().getClassMetadata( entityRecord.getEntityName() ); + IBoxContext jdbcContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcContext ); + ORMApp ormApp = ormContext.getORMApp(); + String entityName = getEntityName( entity ); + EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); + ClassMetadata metadata = session.getSessionFactory().getClassMetadata( entityRecord.getEntityName() ); return metadata.getIdentifier( entity ); } @@ -323,7 +325,7 @@ public static Object getEntityIdentifier( IClassRunnable entity, IBoxContext con */ public void shutdownApp( IBoxContext context ) { this.shutdownApp( ORMService.getAppNameFromContext( context ) ); - context.getRequestContext().removeAttachment( ORMKeys.ORMRequestContext ); + context.removeAttachment( ORMKeys.ORMContext ); } /** @@ -345,8 +347,8 @@ public void shutdownApp( Key uniqueAppName ) { return; // No context to remove from } RequestBoxContext requestContext = context.getRequestContext(); - if ( requestContext != null && requestContext.hasAttachment( ORMKeys.ORMRequestContext ) ) { - requestContext.removeAttachment( ORMKeys.ORMRequestContext ); + if ( requestContext != null && requestContext.hasAttachment( ORMKeys.ORMContext ) ) { + requestContext.removeAttachment( ORMKeys.ORMContext ); } } @@ -359,6 +361,9 @@ public void shutdownApp( Key uniqueAppName ) { */ public ORMApp reloadApp( IBoxContext context ) { RequestBoxContext requestContext = context instanceof RequestBoxContext castedContext ? castedContext : context.getRequestContext(); + if ( requestContext == null ) { + throw new BoxRuntimeException( "No request context available to reload ORM application." ); + } shutdownApp( requestContext ); return startupApp( requestContext, diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityDelete.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityDelete.java index 958befa7..67dcdd56 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityDelete.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityDelete.java @@ -22,11 +22,12 @@ import org.hibernate.Session; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.types.Argument; @@ -58,9 +59,10 @@ public EntityDelete() { public String _invoke( IBoxContext context, ArgumentsScope arguments ) { IClassRunnable entity = ( IClassRunnable ) arguments.get( ORMKeys.entity ); String entityName = getEntityName( entity ); - ORMApp ormApp = ORMRequestContext.getForContext( context.getRequestContext() ).getORMApp(); + ORMContext ormContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); + ORMApp ormApp = ormContext.getORMApp(); EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context.getRequestContext() ).getSession( entityRecord.getDatasource() ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); session.delete( entityName, entity ); diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoad.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoad.java index 132af7b2..56fbad52 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoad.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoad.java @@ -20,6 +20,7 @@ import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.dynamic.casters.StringCaster; @@ -102,6 +103,7 @@ public EntityLoad() { * @argument.options A struct of options to modify the load operation. See below for supported options. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); if ( arguments.get( ORMKeys.uniqueOrOrder ) != null && arguments.get( ORMKeys.options ) == null && arguments.get( ORMKeys.uniqueOrOrder ) instanceof IStruct ) { // If the uniqueOrOrder is a struct, we need to move it to options @@ -113,26 +115,26 @@ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { if ( arguments.containsKey( ORMKeys.idOrFilter ) ) { boolean idIsSimpleValue = StringCasterStrict.attempt( arguments.get( ORMKeys.idOrFilter ) ).wasSuccessful(); if ( idIsSimpleValue ) { - return loadEntityById( context, arguments ); + return loadEntityById( jdbcBoxContext, arguments ); } } // EITHER: No filter or was ID provided, so load all entities as an array... // OR a non-simple value was provided (i.e. a struct or array), so load by filter. - return loadEntitiesByFilter( context, arguments ); + return loadEntitiesByFilter( jdbcBoxContext, arguments ); } /** * Load an entity or array of entities by ID. * - * @param context Context in which the BIF was invoked. + * @param context JDBC context in which the BIF was invoked. * @param arguments Arguments scope of the BIF. */ private Object loadEntityById( IBoxContext context, ArgumentsScope arguments ) { if ( BooleanCaster.cast( arguments.getOrDefault( ORMKeys.uniqueOrOrder, "false" ) ) ) { - return ormService.getORMAppByContext( context ).loadEntityById( context.getRequestContext(), arguments.getAsString( ORMKeys.entityName ), + return ormService.getORMAppByContext( context ).loadEntityById( context, arguments.getAsString( ORMKeys.entityName ), arguments.get( ORMKeys.idOrFilter ) ); } - var entity = ormService.getORMAppByContext( context ).loadEntityById( context.getRequestContext(), arguments.getAsString( ORMKeys.entityName ), + var entity = ormService.getORMAppByContext( context ).loadEntityById( context, arguments.getAsString( ORMKeys.entityName ), arguments.get( ORMKeys.idOrFilter ) ); return entity == null ? Array.EMPTY : Array.of( entity ); } @@ -140,14 +142,14 @@ private Object loadEntityById( IBoxContext context, ArgumentsScope arguments ) { /** * Load an array of entities by filter criteria. * - * @param context Context in which the BIF was invoked. + * @param context JDBC context in which the BIF was invoked. * @param arguments Arguments scope of the BIF. */ private Object loadEntitiesByFilter( IBoxContext context, ArgumentsScope arguments ) { IStruct options = buildCriteriaOptions( arguments ); IStruct filter = arguments.getAsStruct( ORMKeys.idOrFilter ); - Array results = ormService.getORMAppByContext( context ).loadEntitiesByFilter( context.getRequestContext(), + Array results = ormService.getORMAppByContext( context ).loadEntitiesByFilter( context, arguments.getAsString( ORMKeys.entityName ), filter, options ); if ( options.getAsBoolean( ORMKeys.unique ) ) { return results.isEmpty() ? null : results.getFirst(); diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByExample.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByExample.java index 79b3691a..1cb98332 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByExample.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByExample.java @@ -24,12 +24,12 @@ import org.hibernate.criterion.Example; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.context.RequestBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.types.Argument; @@ -59,17 +59,20 @@ public EntityLoadByExample() { */ @SuppressWarnings( { "deprecation", "unchecked" } ) public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - RequestBoxContext requestContext = context.getRequestContext(); - ORMApp ormApp = ORMRequestContext.getForContext( requestContext ).getORMApp(); - Object sampleEntity = arguments.get( ORMKeys.sampleEntity ); - Boolean unique = arguments.getAsBoolean( ORMKeys.unique ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); + ORMApp ormApp = ormContext.getORMApp(); + Object sampleEntity = arguments.get( ORMKeys.sampleEntity ); + Boolean unique = arguments.getAsBoolean( ORMKeys.unique ); + if ( ! ( sampleEntity instanceof IClassRunnable ) ) { throw new BoxRuntimeException( "Sample entity must be a valid entity" ); } + IClassRunnable workingEntity = ( IClassRunnable ) sampleEntity; String entityName = getEntityName( workingEntity ); EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( requestContext ).getSession( entityRecord.getDatasource() ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); Criteria criteria = session.createCriteria( entityName ); Example example = Example.create( workingEntity ); criteria.add( example ); diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByPK.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByPK.java index f7557187..c7b2c5d4 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByPK.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityLoadByPK.java @@ -22,6 +22,7 @@ import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; @@ -69,9 +70,10 @@ public EntityLoadByPK() { * @argument.unique Not implemented. In BoxLang, a single entity is always returned. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - String entityName = arguments.getAsString( ORMKeys.entity ); - Object keyValue = arguments.get( Key.id ); + String entityName = arguments.getAsString( ORMKeys.entity ); + Object keyValue = arguments.get( Key.id ); - return ormService.getORMAppByContext( context ).loadEntityById( context.getRequestContext(), entityName, keyValue ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + return ormService.getORMAppByContext( context ).loadEntityById( jdbcBoxContext, entityName, keyValue ); } } diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityMerge.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityMerge.java index d4249af8..0827ecb0 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityMerge.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityMerge.java @@ -21,11 +21,13 @@ import org.hibernate.Session; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMApp; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.types.Argument; @@ -55,8 +57,11 @@ public EntityMerge() { public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { IClassRunnable entity = ( IClassRunnable ) arguments.get( ORMKeys.entity ); String entityName = getEntityName( entity ); - EntityRecord entityRecord = ormService.getORMAppByContext( context ).lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context.getRequestContext() ).getSession( entityRecord.getDatasource() ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); + ORMApp ormApp = ormContext.getORMApp(); + EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); return session.merge( entity ); } diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityNew.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityNew.java index d4234442..074dbef3 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityNew.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityNew.java @@ -22,11 +22,12 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; @@ -63,7 +64,8 @@ public EntityNew() { * @argument.ignoreExtras If false, an error will be thrown if properties are provided that do not exist on the entity. Not implemented. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - ORMApp ormApp = ORMRequestContext.getForContext( context.getRequestContext() ).getORMApp(); + ORMContext ormContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); + ORMApp ormApp = ormContext.getORMApp(); String entityName = arguments.getAsString( ORMKeys.entityName ); EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); IStruct properties = arguments.containsKey( Key.properties ) ? arguments.getAsStruct( Key.properties ) : Struct.EMPTY; diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityReload.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityReload.java index 19f0a281..c80e7381 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityReload.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityReload.java @@ -19,11 +19,12 @@ import java.util.Set; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.IBoxContext.ScopeSearchResult; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; @@ -51,7 +52,8 @@ public EntityReload() { * @argument.entity The entity instance to reload. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - Object entity = arguments.get( ORMKeys.entity ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + Object entity = arguments.get( ORMKeys.entity ); if ( entity instanceof String variableName ) { ScopeSearchResult entityLookup = context.scopeFindNearby( Key.of( ( String ) variableName ), null, true ); if ( entityLookup == null ) { @@ -59,8 +61,8 @@ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { } entity = entityLookup.value(); } - ORMRequestContext - .getForContext( context.getRequestContext() ) + ORMContext + .getForContext( jdbcBoxContext ) .getSession() .refresh( entity ); return entity; diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntitySave.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntitySave.java index 5a1590f6..9ef4e7a7 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntitySave.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntitySave.java @@ -22,11 +22,12 @@ import org.hibernate.Session; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ArgumentsScope; @@ -60,9 +61,10 @@ public EntitySave() { public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { IClassRunnable entity = ( IClassRunnable ) arguments.get( ORMKeys.entity ); String entityName = getEntityName( entity ); - ORMApp ormApp = ORMRequestContext.getForContext( context.getRequestContext() ).getORMApp(); + ORMContext ormContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); + ORMApp ormApp = ormContext.getORMApp(); EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context.getRequestContext() ).getSession( entityRecord.getDatasource() ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); Boolean forceInsert = BooleanCaster.cast( arguments.getOrDefault( ORMKeys.forceinsert, false ) ); if ( forceInsert ) { diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityToQuery.java b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityToQuery.java index 8a526a3b..18d1d119 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/EntityToQuery.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/EntityToQuery.java @@ -20,12 +20,13 @@ import java.util.List; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.modules.orm.mapping.inspectors.IPropertyMeta; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; @@ -59,7 +60,8 @@ public EntityToQuery() { * @argument.name The name of the entity. Required if `entity` is an array. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - ORMApp ormApp = ORMRequestContext.getForContext( context.getRequestContext() ).getORMApp(); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMApp ormApp = ORMContext.getForContext( jdbcBoxContext ).getORMApp(); EntityRecord entityRecord = null; String entityName = arguments.containsKey( Key._name ) ? arguments.getAsString( Key._name ) diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMClearSession.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMClearSession.java index 1caaddf2..9eb25b59 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMClearSession.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMClearSession.java @@ -19,10 +19,11 @@ import java.util.Set; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; @@ -53,8 +54,9 @@ public ORMClearSession() { * @return True if the session was cleared, false otherwise. */ public Boolean _invoke( IBoxContext context, ArgumentsScope arguments ) { - Key datasourceName = Key.of( StringCaster.attempt( arguments.get( ORMKeys.datasource ) ).getOrDefault( "" ) ); - ORMRequestContext ormContext = ORMRequestContext.getForContext( context.getRequestContext() ); + Key datasourceName = Key.of( StringCaster.attempt( arguments.get( ORMKeys.datasource ) ).getOrDefault( "" ) ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); // If no ORM app found then ignore if ( ormContext.hasORMApp() ) { diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseAllSessions.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseAllSessions.java index caa86be3..5f1b3d87 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseAllSessions.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseAllSessions.java @@ -17,9 +17,10 @@ */ package ortus.boxlang.modules.orm.bifs; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.scopes.ArgumentsScope; @BoxBIF @@ -32,7 +33,8 @@ public class ORMCloseAllSessions extends BaseORMBIF { * @param arguments Argument scope for the BIF. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - ORMRequestContext.getForContext( context.getRequestContext() ).closeAllSessions(); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext.getForContext( jdbcBoxContext ).closeAllSessions(); return null; } diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseSession.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseSession.java index 514a7e2e..5c9ee02f 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseSession.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMCloseSession.java @@ -19,10 +19,11 @@ import java.util.Set; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; @@ -51,8 +52,9 @@ public ORMCloseSession() { * @argument.datasource The datasource on which to close the current session. If not provided, the default datasource will be used. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); - String datasourceName = StringCaster.attempt( arguments.get( ORMKeys.datasource ) ).getOrDefault( "" ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormRequestContext = ORMContext.getForContext( jdbcBoxContext ); + String datasourceName = StringCaster.attempt( arguments.get( ORMKeys.datasource ) ).getOrDefault( "" ); ormRequestContext.closeSession( !datasourceName.isBlank() ? Key.of( datasourceName ) : null ); return null; diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictCollection.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictCollection.java index 9fd901e0..8fa06f6f 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictCollection.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictCollection.java @@ -24,11 +24,12 @@ import org.hibernate.SessionFactory; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.GenericCaster; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.types.Argument; @@ -65,9 +66,11 @@ public ORMEvictCollection() { public String _invoke( IBoxContext context, ArgumentsScope arguments ) { String entityName = arguments.getAsString( ORMKeys.entityName ); String primaryKey = arguments.getAsString( ORMKeys.primaryKey ); - ORMApp ormApp = ORMRequestContext.getForContext( context.getRequestContext() ).getORMApp(); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); + ORMApp ormApp = ormContext.getORMApp(); EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context.getRequestContext() ).getSession( entityRecord.getDatasource() ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); SessionFactory factory = session.getSessionFactory(); // Fix casing. entityName = entityRecord.getEntityName(); diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictEntity.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictEntity.java index a19afe2e..bd95a0f7 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictEntity.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictEntity.java @@ -24,11 +24,12 @@ import org.hibernate.SessionFactory; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.GenericCaster; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.types.Argument; @@ -62,9 +63,11 @@ public ORMEvictEntity() { public String _invoke( IBoxContext context, ArgumentsScope arguments ) { String entityName = arguments.getAsString( ORMKeys.entityName ); String primaryKey = arguments.getAsString( ORMKeys.primaryKey ); - ORMApp ormApp = ORMRequestContext.getForContext( context.getRequestContext() ).getORMApp(); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); + ORMApp ormApp = ormContext.getORMApp(); EntityRecord entityRecord = ormApp.lookupEntity( entityName, true ); - Session session = ORMRequestContext.getForContext( context.getRequestContext() ).getSession( entityRecord.getDatasource() ); + Session session = ormContext.getSession( entityRecord.getDatasource() ); SessionFactory factory = session.getSessionFactory(); // Fix casing. entityName = entityRecord.getEntityName(); diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictQueries.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictQueries.java index edc65c3f..28c836be 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictQueries.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMEvictQueries.java @@ -19,10 +19,11 @@ import org.hibernate.SessionFactory; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; @@ -55,10 +56,11 @@ public ORMEvictQueries() { * @argument.datasource The name of the datasource on which to evict the cache. If not provided, the default datasource will be used. */ public String _invoke( IBoxContext context, ArgumentsScope arguments ) { - String cacheName = arguments.getAsString( ORMKeys.cacheName ); - String datasourceName = arguments.getAsString( ORMKeys.datasource ); - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); - SessionFactory factory = null; + String cacheName = arguments.getAsString( ORMKeys.cacheName ); + String datasourceName = arguments.getAsString( ORMKeys.datasource ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormRequestContext = ORMContext.getForContext( jdbcBoxContext ); + SessionFactory factory = null; if ( datasourceName == null ) { factory = ormRequestContext.getSession().getSessionFactory(); } else { diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlush.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlush.java index cb29dc59..42d06649 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlush.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlush.java @@ -19,10 +19,11 @@ import org.hibernate.Session; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.types.Argument; @@ -48,7 +49,9 @@ public ORMFlush() { * @argument.datasource The datasource on which to flush the current session. If not provided, the default datasource will be used. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - Session session = ORMRequestContext.getForContext( context.getRequestContext() ).getSession(); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); + Session session = ormContext.getSession(); session.flush(); // @TODO: Announce 'onFlush' event return null; diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlushAll.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlushAll.java index 5479396b..d1e2e4f9 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlushAll.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMFlushAll.java @@ -17,9 +17,10 @@ */ package ortus.boxlang.modules.orm.bifs; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.scopes.ArgumentsScope; @BoxBIF @@ -32,7 +33,8 @@ public class ORMFlushAll extends BaseORMBIF { * @param arguments Argument scope for the BIF. */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormRequestContext = ORMContext.getForContext( jdbcBoxContext ); ormRequestContext.getSessions().forEach( ( key, session ) -> session.flush() ); // @TODO: Announce 'onFlush' event return null; diff --git a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMGetSession.java b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMGetSession.java index b7285d41..c544f4c6 100644 --- a/src/main/java/ortus/boxlang/modules/orm/bifs/ORMGetSession.java +++ b/src/main/java/ortus/boxlang/modules/orm/bifs/ORMGetSession.java @@ -21,10 +21,11 @@ import org.hibernate.Session; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; @@ -53,11 +54,14 @@ public ORMGetSession() { * @argument.datasource The name of the datasource to retrieve the Session for. If not specified, the Application's default datasource is used. */ public Session _invoke( IBoxContext context, ArgumentsScope arguments ) { - String datasourceName = StringCaster.attempt( arguments.get( ORMKeys.datasource ) ).getOrDefault( "" ); + String datasourceName = StringCaster.attempt( arguments.get( ORMKeys.datasource ) ).getOrDefault( "" ); + IBoxContext jdbcBoxContext = context.getParentOfType( IJDBCCapableContext.class ); + ORMContext ormContext = ORMContext.getForContext( jdbcBoxContext ); + if ( !datasourceName.isBlank() ) { - return ORMRequestContext.getForContext( context.getRequestContext() ).getSession( Key.of( datasourceName ) ); + return ormContext.getSession( Key.of( datasourceName ) ); } - return ORMRequestContext.getForContext( context.getRequestContext() ).getSession(); + return ormContext.getSession(); } } diff --git a/src/main/java/ortus/boxlang/modules/orm/config/ORMConfig.java b/src/main/java/ortus/boxlang/modules/orm/config/ORMConfig.java index f614b078..70a4a893 100644 --- a/src/main/java/ortus/boxlang/modules/orm/config/ORMConfig.java +++ b/src/main/java/ortus/boxlang/modules/orm/config/ORMConfig.java @@ -35,6 +35,7 @@ import ortus.boxlang.modules.orm.config.naming.MacroCaseNamingStrategy; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.config.segments.CacheConfig; +import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.RequestBoxContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.interop.DynamicObject; @@ -253,9 +254,9 @@ public class ORMConfig { public boolean enableThreadedMapping = true; /** - * Application context used for class lookups in naming strategies, event handlers, etc. + * Boxlang context used for class lookups in naming strategies, event handlers, etc. */ - private RequestBoxContext requestContext; + private IBoxContext context; /** * The instantiated naming strategy object. @@ -267,13 +268,13 @@ public class ORMConfig { * * @param properties Struct of ORM configuration properties. */ - public ORMConfig( IStruct properties, RequestBoxContext context ) { + public ORMConfig( IStruct properties, IBoxContext context ) { this.logger = runtime.getLoggingService().getLogger( "orm" ); if ( properties == null ) { properties = new Struct(); } - this.requestContext = context; + this.context = context; runtime.getInterceptorService().announce( ORMKeys.EVENT_ORM_PRE_CONFIG_LOAD, Struct.of( Key.properties, properties, @@ -295,8 +296,12 @@ public ORMConfig( IStruct properties, RequestBoxContext context ) { * * @return ORMConfig object or null if ORM is not enabled or no ORM settings are present in the application settings. */ - public static ORMConfig loadFromContext( RequestBoxContext context ) { - IStruct appSettings = ( IStruct ) context.getConfigItem( Key.applicationSettings ); + public static ORMConfig loadFromContext( IBoxContext context ) { + RequestBoxContext requestContext = context.getRequestContext(); + if ( requestContext == null ) { + return null; + } + IStruct appSettings = ( IStruct ) requestContext.getConfigItem( Key.applicationSettings ); if ( !appSettings.containsKey( ORMKeys.ORMEnabled ) || !BooleanCaster.cast( appSettings.getOrDefault( ORMKeys.ORMEnabled, false ) ) ) { @@ -440,8 +445,8 @@ private void process( IStruct properties ) { * Read the default datasource name from application settings. */ private Key getAppDefaultDatasource() { - Key defaultDatasource = Key.of( ( String ) this.requestContext.getConfigItems( new Key[] { Key.defaultDatasource } ) ); - IStruct configDatasources = ( IStruct ) this.requestContext.getConfigItems( new Key[] { Key.datasources } ); + Key defaultDatasource = Key.of( ( String ) this.context.getConfigItems( new Key[] { Key.defaultDatasource } ) ); + IStruct configDatasources = ( IStruct ) this.context.getConfigItems( new Key[] { Key.datasources } ); if ( !defaultDatasource.isEmpty() && configDatasources.containsKey( defaultDatasource ) ) { return defaultDatasource; } else if ( !defaultDatasource.isEmpty() ) { @@ -479,7 +484,7 @@ private void setEntityPaths( Object entityPaths ) { public Configuration toHibernateConfig() { // Load the event handler class if it is specified, else null DynamicObject eventHandlerClass = this.eventHandler != null - ? loadBoxLangClassByFQN( this.requestContext, this.eventHandler ) + ? loadBoxLangClassByFQN( this.eventHandler ) : null; BootstrapServiceRegistry bootstrapRegistry = new BootstrapServiceRegistryBuilder() .applyIntegrator( new EventListener( eventHandlerClass ) ) @@ -611,26 +616,25 @@ private PhysicalNamingStrategy getNamingStrategyForName( String name ) { * The "class" naming strategy allows apps to define their own naming strategy by * providing a full box class path. */ - default -> new BoxLangClassNamingStrategy( loadBoxLangClassByFQN( this.requestContext, name ) ); + default -> new BoxLangClassNamingStrategy( loadBoxLangClassByFQN( name ) ); }; } /** * Load a BoxLang class by its fully-qualified name. * - * @param context The current request context. - * @param fqn The fully-qualified name of the class to load. + * @param fqn The fully-qualified name of the class to load. * * @return The loaded class. */ - private DynamicObject loadBoxLangClassByFQN( RequestBoxContext context, String fqn ) { + private DynamicObject loadBoxLangClassByFQN( String fqn ) { return CLASS_LOCATOR.load( - context, + this.context, fqn, ClassLocator.BX_PREFIX, true, - context.getCurrentImports() - ).invokeConstructor( context ); + this.context.getCurrentImports() + ).invokeConstructor( this.context ); } /** @@ -640,6 +644,9 @@ public String getJCacheProviderClassPath() { return "ortus.boxlang.modules.orm.hibernate.cache.BoxHibernateCachingProvider"; } + /** + * Get the default JCache properties for Hibernate. + */ public Properties getJCacheDefaultProperties() { Properties properties = new Properties(); properties.setProperty( "hibernate.cache.region_prefix", datasource.getName() + "_" ); diff --git a/src/main/java/ortus/boxlang/modules/orm/config/ORMKeys.java b/src/main/java/ortus/boxlang/modules/orm/config/ORMKeys.java index 7bfb0cfc..fb81c6c6 100644 --- a/src/main/java/ortus/boxlang/modules/orm/config/ORMKeys.java +++ b/src/main/java/ortus/boxlang/modules/orm/config/ORMKeys.java @@ -32,7 +32,7 @@ public class ORMKeys { // Various keys used as context attachments public static final Key ORMService = Key.of( "ORMService" ); public static final Key RequestListener = Key.of( "RequestListener" ); - public static final Key ORMRequestContext = Key.of( "ORMRequestContext" ); + public static final Key ORMContext = Key.of( "ORMContext" ); public static final Key ORMApp = Key.of( "ORMApp" ); public static final Key TransactionManager = Key.of( "TransactionManager" ); public static final Key ORM = Key.of( "ORM" ); diff --git a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxClassInstantiator.java b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxClassInstantiator.java index 0c28dedf..dd36f19b 100644 --- a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxClassInstantiator.java +++ b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxClassInstantiator.java @@ -30,7 +30,7 @@ import org.hibernate.tuple.entity.EntityMetamodel; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.modules.orm.mapping.inspectors.ClassicPropertyMeta; @@ -234,7 +234,7 @@ private Key getMethodName( String operationPrefix, IStruct associationMeta ) { @Override public Object instantiate( Serializable id ) { - ORMApp ormApp = ORMRequestContext.getForContext( RequestBoxContext.getCurrent() ).getORMApp(); + ORMApp ormApp = ORMContext.getForContext( RequestBoxContext.getCurrent() ).getORMApp(); EntityRecord entityRecord = ormApp.lookupEntity( this.entityName, true ); return instantiate( null, // Will automatically use the current context diff --git a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxLazyInitializer.java b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxLazyInitializer.java index 8f113e9c..db4bb272 100644 --- a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxLazyInitializer.java +++ b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxLazyInitializer.java @@ -26,7 +26,7 @@ import org.hibernate.tuple.entity.EntityMetamodel; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.mapping.EntityRecord; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.RequestBoxContext; @@ -60,7 +60,7 @@ public BoxLazyInitializer( String entityName, Serializable id, SharedSessionCont this.mappingInfo = mappingInfo; this.context = RequestBoxContext.getCurrent(); - this.ormApp = ORMRequestContext.getForContext( context ).getORMApp(); + this.ormApp = ORMContext.getForContext( context ).getORMApp(); this.entityRecord = ormApp.lookupEntity( entityName, true ); SessionFactoryImplementor sessionFactoryImpl = ( SessionFactoryImplementor ) ormApp.getSessionFactoryOrThrow( this.entityRecord.getDatasource() ); diff --git a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxPropertyGetter.java b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxPropertyGetter.java index 7d2293d3..e0f595e7 100644 --- a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxPropertyGetter.java +++ b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxPropertyGetter.java @@ -30,6 +30,7 @@ import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.logging.BoxLangLogger; import ortus.boxlang.runtime.runnables.IClassRunnable; @@ -61,7 +62,7 @@ public BoxPropertyGetter( IBoxContext context, Property mappedProperty, Persiste this.logger = runtime.getLoggingService().getLogger( "orm" ); this.mappedProperty = mappedProperty; this.mappedEntity = mappedEntity; - this.context = context; + this.context = context.getParentOfType( IJDBCCapableContext.class ); } @Override @@ -72,7 +73,7 @@ public Object get( Object owner ) { return castRunnable.getVariablesScope().get( mappedProperty.getName() ); } else { // Otherwise we assume this is a primary key lookup and load the entity to get the property - return ormService.getORMAppByContext( context ).loadEntityById( context.getRequestContext(), mappedEntity.getEntityName(), owner ) + return ormService.getORMAppByContext( context ).loadEntityById( context, mappedEntity.getEntityName(), owner ) .get( mappedProperty.getName() ); } } diff --git a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxProxy.java b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxProxy.java index bb6c49c9..33224dc9 100644 --- a/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxProxy.java +++ b/src/main/java/ortus/boxlang/modules/orm/hibernate/BoxProxy.java @@ -21,7 +21,6 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Set; @@ -49,7 +48,7 @@ /** * Boxlang class proxy. - * + * * @since 1.0.0 */ public class BoxProxy implements IClassRunnable, HibernateProxy { @@ -160,21 +159,6 @@ public List getImports() { return getRunnable().getImports(); } - @Override - public Object getRunnableAST() { - return getRunnable().getRunnableAST(); - } - - @Override - public long getRunnableCompileVersion() { - return getRunnable().getRunnableCompileVersion(); - } - - @Override - public LocalDateTime getRunnableCompiledOn() { - return getRunnable().getRunnableCompiledOn(); - } - @Override public ResolvedFilePath getRunnablePath() { return getRunnable().getRunnablePath(); diff --git a/src/main/java/ortus/boxlang/modules/orm/hibernate/EntityTuplizer.java b/src/main/java/ortus/boxlang/modules/orm/hibernate/EntityTuplizer.java index 60856d1f..99609763 100644 --- a/src/main/java/ortus/boxlang/modules/orm/hibernate/EntityTuplizer.java +++ b/src/main/java/ortus/boxlang/modules/orm/hibernate/EntityTuplizer.java @@ -95,8 +95,7 @@ public String determineConcreteSubclassEntityName( Object entityInstance, Sessio @Override public Class getMappedClass() { - // TODO Auto-generated method stub - throw new UnsupportedOperationException( "Unimplemented method 'getMappedClass'" ); + return BoxProxy.class; } @Override diff --git a/src/main/java/ortus/boxlang/modules/orm/interceptors/RequestListener.java b/src/main/java/ortus/boxlang/modules/orm/interceptors/RequestListener.java index 50b122ec..e8d4b7fb 100644 --- a/src/main/java/ortus/boxlang/modules/orm/interceptors/RequestListener.java +++ b/src/main/java/ortus/boxlang/modules/orm/interceptors/RequestListener.java @@ -17,7 +17,7 @@ */ package ortus.boxlang.modules.orm.interceptors; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.config.ORMConfig; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.context.RequestBoxContext; @@ -49,12 +49,10 @@ public void configure( IStruct properties ) { @InterceptionPoint public void onRequestStart( IStruct args ) { - logger.debug( "onRequestStart - Starting up ORM request" ); } @InterceptionPoint public void onRequestEnd( IStruct args ) { - logger.debug( "onRequestEnd - Shutting down ORM request" ); RequestBoxContext context = args.getAs( RequestBoxContext.class, Key.context ); ORMConfig config = ORMConfig.loadFromContext( context ); @@ -63,13 +61,14 @@ public void onRequestEnd( IStruct args ) { return; } - if ( !context.hasAttachment( ORMKeys.ORMRequestContext ) ) { + if ( !context.hasAttachment( ORMKeys.ORMContext ) ) { logger.warn( "No ORM request context; did the request startup fail for some reason?" ); return; } - ORMRequestContext ormRequestContext = context.getAttachment( ORMKeys.ORMRequestContext ); + logger.debug( "onRequestEnd - Shutting down ORM request" ); + ORMContext ormRequestContext = context.getAttachment( ORMKeys.ORMContext ); ormRequestContext.shutdown(); - context.removeAttachment( ORMKeys.ORMRequestContext ); + context.removeAttachment( ORMKeys.ORMContext ); } } diff --git a/src/main/java/ortus/boxlang/modules/orm/interceptors/TransactionManager.java b/src/main/java/ortus/boxlang/modules/orm/interceptors/TransactionManager.java index e9eb70f8..8dceefda 100644 --- a/src/main/java/ortus/boxlang/modules/orm/interceptors/TransactionManager.java +++ b/src/main/java/ortus/boxlang/modules/orm/interceptors/TransactionManager.java @@ -20,12 +20,12 @@ import org.hibernate.Session; import ortus.boxlang.modules.orm.ORMApp; -import ortus.boxlang.modules.orm.ORMRequestContext; +import ortus.boxlang.modules.orm.ORMContext; import ortus.boxlang.modules.orm.ORMService; import ortus.boxlang.modules.orm.config.ORMConfig; import ortus.boxlang.modules.orm.config.ORMKeys; import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.context.RequestBoxContext; +import ortus.boxlang.runtime.context.IJDBCCapableContext; import ortus.boxlang.runtime.events.BaseInterceptor; import ortus.boxlang.runtime.events.InterceptionPoint; import ortus.boxlang.runtime.scopes.Key; @@ -59,13 +59,13 @@ public void configure( IStruct properties ) { @InterceptionPoint public void onTransactionBegin( IStruct args ) { IBoxContext context = args.getAs( IBoxContext.class, Key.context ); - if ( !isORMEnabled( context.getRequestContext() ) ) { + if ( !isORMEnabled( context.getParentOfType( IJDBCCapableContext.class ) ) ) { return; } - ORMApp ormApp = ormService.getORMAppByContext( context ); - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); - ORMConfig config = ormRequestContext.getConfig(); + ORMApp ormApp = ormService.getORMAppByContext( context ); + ORMContext ormRequestContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); + ORMConfig config = ormRequestContext.getConfig(); ormApp.getDatasources().forEach( ( datasource ) -> { Session ormSession = ormRequestContext.getSession( datasource ); @@ -105,12 +105,12 @@ public void onTransactionBegin( IStruct args ) { @InterceptionPoint public void onTransactionCommit( IStruct args ) { IBoxContext context = args.getAs( IBoxContext.class, Key.context ); - if ( !isORMEnabled( context.getRequestContext() ) ) { + if ( !isORMEnabled( context.getParentOfType( IJDBCCapableContext.class ) ) ) { return; } - ORMApp ormApp = ormService.getORMAppByContext( context ); - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); + ORMApp ormApp = ormService.getORMAppByContext( context ); + ORMContext ormRequestContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); ormApp.getDatasources().forEach( datasource -> { Session ormSession = ormRequestContext.getSession( datasource ); @@ -131,13 +131,13 @@ public void onTransactionCommit( IStruct args ) { @InterceptionPoint public void onTransactionRollback( IStruct args ) { IBoxContext context = args.getAs( IBoxContext.class, Key.context ); - if ( !isORMEnabled( context.getRequestContext() ) ) { + if ( !isORMEnabled( context.getParentOfType( IJDBCCapableContext.class ) ) ) { return; } - ORMApp ormApp = ormService.getORMAppByContext( context ); - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); - ORMConfig config = ormRequestContext.getConfig(); + ORMApp ormApp = ormService.getORMAppByContext( context ); + ORMContext ormRequestContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); + ORMConfig config = ormRequestContext.getConfig(); ormApp.getDatasources().forEach( ( datasource ) -> { // FYI: Lucee's implementation actually waits until transaction END to rollback and clear the session. @@ -175,12 +175,12 @@ public void onTransactionRollback( IStruct args ) { @InterceptionPoint public void onTransactionEnd( IStruct args ) { IBoxContext context = args.getAs( IBoxContext.class, Key.context ); - if ( !isORMEnabled( context.getRequestContext() ) ) { + if ( !isORMEnabled( context.getParentOfType( IJDBCCapableContext.class ) ) ) { return; } - ORMApp ormApp = ormService.getORMAppByContext( context ); - ORMRequestContext ormRequestContext = ORMRequestContext.getForContext( context.getRequestContext() ); + ORMApp ormApp = ormService.getORMAppByContext( context ); + ORMContext ormRequestContext = ORMContext.getForContext( context.getParentOfType( IJDBCCapableContext.class ) ); ormApp.getDatasources().forEach( ( datasource ) -> { Session ormSession = ormRequestContext.getSession( datasource ); @@ -201,13 +201,13 @@ public void onTransactionEnd( IStruct args ) { /** * Ensure ORM is enabled for this request before we attempt any transaction processing. * - * @param requestContext Request context which will have an ORM config attached if ORM is enabled. + * @param jdbcContext JDBC-capable context which will have an ORM config attached if ORM is enabled. */ - private boolean isORMEnabled( RequestBoxContext requestContext ) { - if ( requestContext == null ) { + private boolean isORMEnabled( IJDBCCapableContext jdbcContext ) { + if ( jdbcContext == null ) { return false; } - ORMConfig ormConfig = ORMConfig.loadFromContext( requestContext ); + ORMConfig ormConfig = ORMConfig.loadFromContext( jdbcContext ); if ( ormConfig == null ) { // ORM is not enabled for this application return false; diff --git a/src/test/java/ortus/boxlang/modules/orm/bifs/EntityLoadTest.java b/src/test/java/ortus/boxlang/modules/orm/bifs/EntityLoadTest.java index 7d532a6c..5c0aaf5d 100644 --- a/src/test/java/ortus/boxlang/modules/orm/bifs/EntityLoadTest.java +++ b/src/test/java/ortus/boxlang/modules/orm/bifs/EntityLoadTest.java @@ -123,6 +123,32 @@ public void testEntityLoadFilterArray() { } ); } + @DisplayName( "It can search for entities with a filter criteria containing a null value" ) + @Test + public void testEntityLoadFilterWithNull() { + // @formatter:off + instance.executeSource( """ + result = entityLoad( 'cbContentStore', { 'slug' : 'my-expired-content-store', 'expireDate' : null } ); + """, context ); + // @formatter:on + assertThat( variables.get( result ) ).isNotNull(); + assertThat( variables.get( result ) ).isInstanceOf( Array.class ); + assertThat( variables.getAsArray( result ).size() ).isEqualTo( 0 ); + } + + @DisplayName( "It can search for entities with a filter criteria containing a null relationship" ) + @Test + public void testEntityLoadFilterWithNullRelationship() { + // @formatter:off + instance.executeSource( """ + result = entityLoad( 'Vehicle', { 'manufacturer' : null } ); + """, context ); + // @formatter:on + assertThat( variables.get( result ) ).isNotNull(); + assertThat( variables.get( result ) ).isInstanceOf( Array.class ); + assertThat( variables.getAsArray( result ).size() ).isEqualTo( 1 ); + } + @DisplayName( "It can load array of entities by filter criteria, sorting by custom order clause" ) @Test public void testEntityLoadFilterSort() { diff --git a/src/test/java/ortus/boxlang/modules/orm/bifs/ORMExecuteQueryTest.java b/src/test/java/ortus/boxlang/modules/orm/bifs/ORMExecuteQueryTest.java index 4dbeb678..1e2eddc9 100644 --- a/src/test/java/ortus/boxlang/modules/orm/bifs/ORMExecuteQueryTest.java +++ b/src/test/java/ortus/boxlang/modules/orm/bifs/ORMExecuteQueryTest.java @@ -41,7 +41,7 @@ public void testHQLOnly() { Object array = variables.get( result ); assertThat( array ).isInstanceOf( Array.class ); Array a = ( Array ) array; - assertThat( a.size() ).isEqualTo( 4 ); + assertThat( a.size() ).isEqualTo( 5 ); } @DisplayName( "It can run an HQL query on another datasource" ) diff --git a/src/test/java/tools/JDBCTestUtils.java b/src/test/java/tools/JDBCTestUtils.java index cdc7bdb6..b491774d 100644 --- a/src/test/java/tools/JDBCTestUtils.java +++ b/src/test/java/tools/JDBCTestUtils.java @@ -58,7 +58,8 @@ INSERT INTO vehicles (vin,make,model,FK_manufacturer) VALUES ('1HGCM82633A123456','Honda', 'Accord', 42 ), ('2HGCM82633A654321','Honda', 'Civic', 42 ), ('1HGCM82633A789012','Honda', 'Ridgeline', 42 ), - ('9ABAZ85656A776723','Ford', 'Fusion', 1 ) + ('9ABAZ85656A776723','Ford', 'Fusion', 1 ), + ('0SB123','Studebaker', 'Studious', NULL ) """, context ); //@formatter:off diff --git a/src/test/resources/app/Application.bx b/src/test/resources/app/Application.bx index 9f5f84bc..03412538 100644 --- a/src/test/resources/app/Application.bx +++ b/src/test/resources/app/Application.bx @@ -7,19 +7,19 @@ class{ "TestDB" = { "driver" = "mysql", "database" = "test", - "host" = "${env.DB_HOST:127.0.0.1}", - "port" = "${env.DB_PORT:3306}", - "username" = "${env.DB_USER:root}", - "password" = "${env.DB_PASSWORD:root}", + "host" = "#getSystemSetting( "DB_HOST", "127.0.0.1" )#", + "port" = "#getSystemSetting( "DB_PORT", 3306 )#", + "username" = "#getSystemSetting( "DB_USER", "root" )#", + "password" = "#getSystemSetting( "DB_PASSWORD", "root" )#", "LeakDetectionThreshold" = 3 }, "dsn2": { "driver" = "mysql", "database" = "dsn2", - "host" = "${env.DB_HOST:127.0.0.1}", - "port" = "${env.DB_PORT:3306}", - "username" = "${env.DB_USER:root}", - "password" = "${env.DB_PASSWORD:root}", + "host" = "#getSystemSetting( "DB_HOST", "127.0.0.1" )#", + "port" = "#getSystemSetting( "DB_PORT", 3306 )#", + "username" = "#getSystemSetting( "DB_USER", "root" )#", + "password" = "#getSystemSetting( "DB_PASSWORD", "root" )#", "LeakDetectionThreshold" = 3 } };