EzQu Session
Preface
At the heart of EzQU is the EzQu Session.
EzQu is an ORM framework built on top of JDBC, abstracting away the underlying JDBC complexities and allowing developers to interact directly with their business model. In Java, interacting with any relational database requires a JDBC connection; in EzQu, this connection is encapsulated within a session object called Db (com.centimia.orm.ezqu.Db).
The Db object is a single-threaded, AutoCloseable object. Its life cycle spans within a Thread, conceptually modeling a "Unit of Work". It serves as the entry point for all CRUD operations and fluent query execution. Under the hood it wraps around 'java.sal.Connection'. To create a session, you must first define an EzquSessionFactory (com.centimia.orm.ezqu.EzquSessionFactory). Each EzquSessionFactory instance manages connections for a single relational database schema or data source.
EzQu Session Factory
An EzquSessionFactory is responsible for providing a thread-bound Session / Connection to a specified underlying datasource. You must define a single instance of the session factory per datasource across the project. If your application interacts with multiple datasources, you must create a separate EzquSessionFactory for each one.
Define with plain java
EzquSessionFactory is not a singleton by design. It is recommended to instantiate it using an IoC framework such as Spring or CCore, which can manage its lifecycle and enforce a singleton instance per datasource.
However, you can also create an EzquSessionFactory manually in plain Java. To do so, you must provide a DataSource (javax.sql.DataSource) during construction. After instantiation, you can configure several parameters, using setters, to control the factory's behaviour, including (but not limited to) connection handling, caching, and session customization.
| Attribute | Usage |
|---|---|
| AcidConfiguration | EzQu supports two modes of transaction control:
This dual-mode design offers flexibility for both lightweight and enterprise-grade architectures. |
| CreateTable | A boolean flag that instructs the factory what to do when the corresponding table does not exist. If set to true, EzQu will automatically create the table during schema validation. |
| dialect | Set the type of DB dialect to use: H2 (default), MYSQL (also for mariaDB), SQLSERVER, ORACLE, DB2, POSTGRES. |
| showSql | Set to true to show the generated sql that is sent to the DB in the log. Used for debugging purposes. |
| transactionIsolation | Determines the isolation level for a single connection. The options here match the options from javax.sql.Connection. Look at the documentation there for more information.This property can only be assigned during construction of the factory. Available Isolations: |
Configure with CCoreFactory
Centimia's CCoreFactory serves as the foundational Inversion of Control (IOC) factory in the Centimia Framework. Refer to the CCoreFactory documentation for detailed implementation guidance. This factory leverages the Configurator component to manage configuration settings. The following example illustrates how to configure the EzquSessionFactory using CCoreFactory.
<Configuration xmlns="www.centimia.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="www.centimia.com http://www.centimia.com/xsd/Configuration.xsd">
<!-- Hikari -->
<Resource managedClass="com.zaxxer.hikari.HikariDataSource" name="HikariDataSource">
<Property name="username" value="${db_username}" />
<Property name="password" value="${db_password}" />
<Property name="maxPoolSize" value="${db_pool_size}" />
<Property name="connectionTimeout" value="180000" />
<Property name="jdbcUrl" value="${jdbcUrl}" />
<Property name="isAutoCommit" value="false" />
<Property name="isRegisterMbeans" value="true" />
<Property name="poolName" value="ccore-pool"/>
</Resource>
<Resource name="com.centimia.EzquSessionFactory" managedClass="com.centimia.orm.ezqu.EzquSessionFactory" creationStrategy="SINGLETON">
<Construct>
<Attribute key="DataSource" type="javax.sql.CommonDataSource" value="ref:HikariDataSource" />
</Construct>
<Property name="showSQL" value="true" />
<Property name="createTable" value="true" />
<Property name="dialect" value="ORACLE" />
<Property name="acidConfig" value="EXTERNAL" />
<Property name="autoCommit" value="false" />
</Resource>
<EnvVars name="vars" env="DEV">
<Property name="DEV" value="">
<Attribute key="db_username" value="username"/>
<Attribute key="db_password" value="password"/>
<Attribute key="jdbcUrl" value="jdbc:oracle:thin:@serveraddr:1521:SCHEMA"/>
<Attribute key="db_pool_size" value="10"/>
</Property>
<Property name="TEST" value="">
<Attribute key="db_username" value="testusername"/>
<Attribute key="db_password" value="testpassword"/>
<Attribute key="jdbcUrl" value="jdbc:oracle:thin:@testserveraddr:1521:SCHEMA"/>
<Attribute key="db_pool_size" value="10"/>
</Property>
<Property name="PROD" value="">
<Attribute key="db_username" value="produsername"/>
<Attribute key="db_password" value="prodpassword"/>
<Attribute key="jdbcUrl" value="jdbc:oracle:thin:@prodserveraddr:1521:SCHEMA"/>
<Attribute key="db_pool_size" value="10"/>
</Property>
</EnvVars>
</Configuration>
Here is an example using a transaction manager. See discussion on Transaction Scope below.
<Configuration xmlns="www.centimia.com"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="www.centimia.com http://www.centimia.com/xsd/Configuration.xsd">
<Resource managedClass="bitronix.tm.Configuration" name="bitronixConfig">
<Construct name="factoryClass" value="bitronix.tm.TransactionManagerServices#getConfiguration"/>
<Accessor name="setServerId" accessor="setServerId" value="btm-node0"/>
<Accessor name="disableJmx" accessor="setDisableJmx" value="true"/>
<Accessor name="logPart1Filename" accessor="setLogPart1Filename" value="./target/btm1.tlog"/>
<Accessor name="logPart2Filename" accessor="setLogPart2Filename" value="./target/btm2.tlog"/>
</Resource>
<Resource managedClass="bitronix.tm.BitronixTransactionManager" name="transactionManager" loadAfter="bitronixConfig">
<Construct name="factoryClass" value="bitronix.tm.TransactionManagerServices#getTransactionManager"/>
<Accessor name="setTransactionTimeout" accessor="setTransactionTimeout" value="40"/>
<ShutdownHook name="shutdown"/>
</Resource>
<Resource managedClass="bitronix.tm.resource.jdbc.PoolingDataSource" name="PoolingDatasource">
<Property name="uniqueName" value="jdbc/mySqlDB"/>
<Property name="minPoolSize" value="2"/>
<Property name="maxPoolSize" value="${db_pool_size}"/>
<Property name="allowLocalTransactions" value="true"/>
<Property name="className" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
<ShutdownHook name="close"/>
</Resource>
<Resource managedClass="java.util.Properties" name="driverProperties">
<Construct name="resource" value="ref:PoolingDatasource#getDriverProperties"/>
<Accessor name="setUser" accessor="setProperty" value="">
<Attribute key="" type="java.lang.String" value="user"/>
<Attribute key="" type="java.lang.String" value="${db_username}"/>
</Accessor>
<Accessor name="setPassword" accessor="setProperty" value="">
<Attribute key="" type="java.lang.String" value="password"/>
<Attribute key="" type="java.lang.String" value="${db_password}"/>
</Accessor>
<Accessor name="setUrl" accessor="setProperty" value="">
<Attribute key="" type="java.lang.String" value="URL"/>
<Attribute key="" type="java.lang.String" value="${jdbcUrl}"/>
</Accessor>
</Resource>
<Resource name="com.centimia.EzquSessionFactory" managedClass="com.centimia.orm.ezqu.EzquSessionFactory" creationStrategy="SINGLETON">
<Construct>
<Attribute key="DataSource" type="javax.sql.CommonDataSource" value="ref:PoolingDatasource" />
</Construct>
<Property name="showSQL" value="true" />
<Property name="createTable" value="true" />
<Property name="dialect" value="MYSQL" />
<Property name="acidConfig" value="EXTERNAL" />
<Property name="autoCommit" value="false" />
<Accessor name="setTransactionStrategy" accessor="setTransactionManager" value="ref:transactionManager"/>
</Resource>
<EnvVars name="vars" env="DEV">
<Property name="DEV" value="">
<Attribute key="db_username" value="username"/>
<Attribute key="db_password" value="password"/>
<Attribute key="jdbcUrl" value="jdbc:mysql://devaddress:3306/db?useUnicode=true&autoCommit=false&characterEncoding=UTF8&useSSL=false"/>
<Attribute key="db_pool_size" value="10"/>
</Property>
<Property name="TEST" value="">
<Attribute key="db_username" value="testusername"/>
<Attribute key="db_password" value="testpassword"/>
<Attribute key="jdbcUrl" value="jdbc:mysql://testaddress:3306/db?useUnicode=true&autoCommit=false&characterEncoding=UTF8&useSSL=false"/>
<Attribute key="db_pool_size" value="10"/>
</Property>
<Property name="PROD" value="">
<Attribute key="db_username" value="produsername"/>
<Attribute key="db_password" value="prodpassword"/>
<Attribute key="jdbcUrl" value="jdbc:mysql://prodaddress:3306/db?useUnicode=true&autoCommit=false&characterEncoding=UTF8&useSSL=false"/>
<Attribute key="db_pool_size" value="10"/>
</Property>
</EnvVars>
</Configuration>
Configure with Spring
EzQu is designed without external dependencies, making it compatible with any Inversion of Control (IOC) framework of your choice. To integrate it, simply provide a java.sql.DataSourceinstance to its constructor and configure any required parameters. Below is an example demonstrating this setup using the Spring framework, which leverages Spring's dependency injection to define beans for both HikariDataSource and EzquSessionFactory.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Load externalized configuration (e.g., database.properties) -->
<context:property-placeholder location="classpath:database.properties" />
<!-- HikariDataSource for Oracle (Main DB) -->
<bean id="HikariDataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
<property name="jdbcUrl" value="${db.jdbcUrl}" />
<property name="maximumPoolSize" value="${db.pool.size}" />
<property name="connectionTimeout" value="180000" />
<property name="isAutoCommit" value="false" />
<property name="poolName" value="spring-pool" />
</bean>
<!-- EzquSessionFactory for Oracle (Main DB) -->
<bean id="com.centimia.EzquSessionFactory" class="com.centimia.orm.ezqu.EzquSessionFactory" scope="singleton">
<constructor-arg name="dataSource" ref="HikariDataSource" />
<property name="showSQL" value="true" />
<property name="createTable" value="true" />
<property name="dialect" value="MYSQL" />
<property name="acidConfig" value="EXTERNAL" />
<property name="autoCommit" value="false" />
</bean>
</beans>
An example using a transaction manager:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="btmConfig" class="bitronix.tm.TransactionManagerServices"
factory-method="getConfiguration">
<property name="serverId" value="spring-btm-node0"/>
<property name="disableJmx" value="true"/>
<property name="logPart1Filename" value="./target/btm1.tlog"/>
<property name="logPart2Filename" value="./target/btm2.tlog"/>
</bean>
<bean id="bitronixTransactionManager" factory-method="getTransactionManager"
class="bitronix.tm.TransactionManagerServices"
depends-on="btmConfig" destroy-method="shutdown"/>
<bean id="dataSource" class="bitronix.tm.resource.jdbc.PoolingDataSource"
init-method="init" destroy-method="close">
<property name="uniqueName" value="jdbc/mySqlDS"/>
<property name="minPoolSize" value="2"/>
<property name="maxPoolSize" value="10"/>
<property name="allowLocalTransactions" value="true"/>
<property name="className" value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
<property name="driverProperties">
<props>
<prop key="user">postgres</prop>
<prop key="password">secret</prop>
<prop key="url">jdbc:postgresql://localhost:5432/mydb</prop>
</props>
</property>
</bean>
<!-- EzquSessionFactory for Oracle (Main DB) -->
<bean id="com.centimia.EzquSessionFactory" class="com.centimia.orm.ezqu.EzquSessionFactory" scope="singleton">
<constructor-arg name="dataSource" ref="dataSource" />
<property name="showSQL" value="true" />
<property name="createTable" value="true" />
<property name="dialect" value="MYSQL" />
<property name="acidConfig" value="EXTERNAL" />
<property name="autoCommit" value="false" />
<property name="transactionManager" ref="bitronixTransactionManager"/>
</bean>
</beans>
Some notes
- Property Placeholder:
- Spring uses
<context:property-placeholder>to load external properties (e.g., database.properties). - Example database.properties:
- Spring uses
db.username=your_db_user
db.password=your_db_password
db.jdbcUrl=jdbc:oracle:thin:@//localhost:1521/ORCL
db.pool.size=10
- Bean Scope:
- scope="singleton" is the default in Spring, but explicitly declared here for clarity.
- Constructor Injection
- EzquSessionFactory is wired via constructor injection
<constructor-arg>
- EzquSessionFactory is wired via constructor injection
Spring Java Configuration (Alternative)
If you prefer Java-based configuration (e.g., @Configuration classes):
@Configuration
public class DataSourceConfig {
@Value("${db.username}")
private String username;
@Value("${db.password}")
private String password;
@Value("${db.jdbcUrl}")
private String jdbcUrl;
@Value("${db.pool.size}")
private int poolSize;
@Bean(destroyMethod = "close")
public HikariDataSource HikariDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setJdbcUrl(jdbcUrl);
dataSource.setMaximumPoolSize(poolSize);
dataSource.setConnectionTimeout(180000);
dataSource.setPoolName("spring-pool");
dataSource.setAutoCommit(false);
return dataSource;
}
@Bean
public EzquSessionFactory mainSessionFactory(HikariDataSource HikariDataSource) {
EzquSessionFactory factory = new EzquSessionFactory(HikariDataSource);
factory.setShowSQL(true);
factory.setCreateTable(true);
factory.setDialect("ORACLE");
factory.setAcidConfig("EXTERNAL");
factory.setAutoCommit(false);
return factory;
}
}
Audit And Debug
Why Auditing SQL Statements Matters: When an ORM generates SQL on the fly, the developer often has little visibility into the exact queries that are sent to the database. This opacity can lead to subtle bugs, unexpected data access patterns, and performance regressions that are hard to diagnose. By inspecting the raw SQL statements and the values bound to them, developers can:
- Verify that the ORM is translating the domain model correctly and that no unintended joins or filters are applied.
- Spot inefficiencies such as duplicate queries, N+1 problems, or overly broad predicates that can degrade performance.
- Ensure that sensitive data is not inadvertently logged or exposed, and that parameterization protects against SQL injection.
- Provide auditors and compliance teams with concrete evidence of how data is accessed and modified, which is essential for regulated environments.
Given these benefits, many teams enable SQL auditing in production or staging builds to maintain full observability over the database interactions orchestrated by the ORM.
To allow such auditing, EzQu automatically binds to an SLF4J logger if one is present, otherwise to java.util.logging. Depending on the configured log level, it outputs error, info, and debug messages. In addition to standard logging, EzQu offers an auditing mode for SQL statements. By setting the session property showSQL to true, EzQu will print each generated SQL statement together with the parameter values it used.
Using a Session (Db)
As mentioned, interacting with any relational database requires a JDBC connection; in EzQu, this connection is encapsulated within a session object called Db (com.centimia.orm.ezqu.Db).
The Db object is AutoCloseable and serves as the entry point for all CRUD operations and fluent query execution. To run in a session, you must first define an EzquSessionFactory (com.centimia.orm.ezqu.EzquSessionFactory). See above section! Each EzquSessionFactory instance manages connections for a single relational database schema or data source.
After you acquire a reference to a SessionFactory, you can execute your code in a Session.
(i.e., a com.centimia.orm.ezqu.Db instance) using its methods. The EzquSessionFactory is thread-aware, meaning it binds the Db session to the current thread. Regardless of where you access the SessionFactory, running in a Db session will use the same instance as long as the operation occurs within the same thread, unless stated otherwise specifically.
Your code runs as a "lamda" supplier/ function in a session factory call. There are two methods to run your code:
runInSession(SessionVoidSupplier supplier, int ... options): runs your code in a DB session without expecting you to return a value.getFromSession(SessionSupplier, int ... options): runs your code in a DB session expecting you to return some kind of value.
options are optional and allow you to tune the session lifecycle to your needs; Details are in the next section. The default behavior is to run your code in a thread-bound session if it exists. If not, it creates a new session, binds it to the thread, and executes your code in it.
sessionFacotry.runInSession(db -> {
try {
// The following will create a table Testtable1 in DB and insert 3 rows into it.
db.insertAll(TestTable1.getSomeData(), Db.FULL_DEPTH);
// Commit the info to the DB. Because we're running on the same connection putting commit here or after the select makes not difference.
db.commit();
// Select all rows from the Testtable1 in the DB
List<TestTable1> rows = db.from(new TestTable1()).select();
...
}
catch (Exception e) {
db.rollback();
}
});
Person me = sessionFacotry.getFromSession(db -> {
final Person res = new Person();
// here we load our person from the db
res = db.from(res).primaryKey().is(1L).selectFirst();
return res; // no need for commit as this is a select
});
This thread-binding mechanism ensures consistency within a single execution thread while providing flexibility for scenarios where independent sessions are required.
Examples:
List<Person> people = sessionFactory.getFromSession(select -> {
Person p = new Person();
return select.from(p).where(p.getIdentification()).is("1234567").select();
});
sessionFactory.runInSession(db -> {
try {
Person p = new Person("1234567", "john", "doe");
db.insert(p);
db.commit();
}
catch (Exception e) {
db.rollback();
}
});
In the previous examples, a basic SELECT and a straightforward INSERT were demonstrated. These are just two simple and foundational examples of EzQu's capabilities. The next chapter will explore all the library's features in detail. For now, note that the INSERT in the example was executed in non-auto-commit mode, requiring an explicit db.commit() call to finalize the transaction. Additionally, your code is actually wrapped in a tryWithResources block which is only accessible within that scope. This ensures proper transaction handling even when the database connection is managed as a scoped resource.
Four session modes are available, SCOPED, CURRENT, NEW, COMMIT, each mixable to let developers manage scope and transaction flow as needed.
- NONE - regular session, create new if not exists
- SCOPED - regular scoped session with commit off
- CURRENT - use existing thread-bound session or exception
- SCOPED, CURRENT - current session scoped with commit off
- NEW - new local session
- SCOPED, NEW - new local session scoped with commit off
- SCOPED, COMMIT - regular scoped session with commit on
- SCOPED, CURRENT, COMMIT - current scoped session with commit on
- SCOPED, NEW, COMMIT - new local scoped session with commit on
// with options the call would look like this:
sessionFacotry.runInSession(db -> {
try {
// The following will create a table Testtable1 in DB and insert 3 rows into it.
db.insertAll(TestTable1.getSomeData(), Db.FULL_DEPTH);
// Commit the info to the DB. Because we're running on the same connection putting commit here or after the select makes not difference.
db.commit();
// Select all rows from the Testtable1 in the DB
List<TestTable1> rows = db.from(new TestTable1()).select();
...
}
catch (Exception e) {
db.rollback();
}
}, SessionOptions.SCOPED, SessionOptions.COMMIT);
Session scope
As noted, EzquSessionFactory binds the Db session to the current thread. Because the factory is a singleton, calls to runInSession or getFromSession reuse the same session within that thread, unless the connection, bound to the thread, is explicitly closed. However, since the session implements AutoCloseable, it is automatically closed when exiting those methods.
This thread-bound behavior raises the question of transaction scope (or session scope). EzquSessionFactory operates in two modes: INTERNAL and EXTERNAL. In EXTERNAL mode, you can either manually manage transactions via commit()/rollback() or delegate transaction handling to an external TransactionManager.
Here's how scope behaves in each mode:
INTERNAL mode:
Every write operation is automatically committed or rolled back. The underlying db connection is set to auto-commit mode and you can't explicitly manage the transaction or session scope; EzQu handles it for you. Here are examples illustrating this:
methodA {
sessionFactory.runInSession(db -> {
// start of connection scope
try {
// do some db work all is committed automatically
methodB(); // call to method
}
catch (Exception any) {
// no need to roll back since rolled back automatically
}
// end of connection scope, connection is closed when we leave here
});
}
// some possibilities for method B
methodB {
// **DANGEROUS** - if you write your code like this
// an error might be thrown in methodA if there are db operations
// after the call to methodB because
// at the start of this method, the session on thread is used.
// at the end of it this session will be closed,
// coming back to methodA, in the commit there the underlying
// db connection is closed, and thus an error is thrown
sessionFactory.runInSession(db -> {
// start of connection scope, using the connection from A
try {
// do some db work, all is committed automatically
}
catch (Exception any) {
// no need to roll back since rolled back automatically
}
// end of connection scope, connection is closed when we leave here
}); // or }, SessionOptions.CURRENT); - use only if there is an existing session
}
methodB {
// here we get the current session without close with resources.
// the danger here being that if method B is called from another
// method that does not manage a session, we will have a leaky
// un-closed session here.
Db db = sessionFactory.currentSession();
try {
// do some db work, all is committed automatically
}
catch (Exception any) {
// no need to roll back since rolled back automatically
}
}
methodB {
// run your code in a new Session thus
// not effecting the one used in methodA.
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
}
catch (Exception any) {
// no need to roll back since rolled back automatically
}
// end of new connection scope, new connection is closed when we leave here
}, SessionOptions.NEW);
}
INTERNAL mode is suitable mainly for small, short-lived CRUD apps, as it places full scoping responsibility on the developer. For larger, more complex projects with shared code and services, the External approach is advised.
EXTERNAL mode with TransactionManager:
A TransactionManager (an external component) wraps around all execution flow. You are not required to call commit() manually. However, you can call rollback() which will only mark the connection as RollbackOnly. The TransactionManager is responsible for executing the rollback during transaction cleanup.
Since TransactionManagers span the entire thread execution, the transaction context is typically available throughout the thread. This means you can share the Db session across multiple methods without worrying about where commit() is called. When you invoke rollback(), it immediately affects the entire transaction but is executed at the end of the transaction lifecycle. When EzquSessionFactory uses a TransactionManager, the auto-close feature of the session does not terminate the connection; you must explicitly close the connection at transaction completion (or include that step in the transaction-ending logic).
In Java, JTA is mainly for 2-phase commits and XA data sources. If your application doesn't need that, we recommend using EXTERNAL mode without a TransactionManager.
EXTERNAL mode without TransactionManager:
You must manually manage transactions. For example, you might start a session in one method, pass it to another via an AutoCloseable connection, and ensure commit() and close() are handled at the outermost scope. While commit() can be called at various points in the execution chain, close() would always be managed in the outermost method otherwise actions may try to be performed on a closed Session and thus cause an error.
Here is an example illustrating this last scenario:
methodA {
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
methodB() // call methodB
// do some more work
db.commit(); // commit occurs only here
}
catch (Exception any) {
db.rollback(); // rollback occurs only here
}
// end of new connection scope, connection is closed when we leave here
}, SessionOptions.SCOPED);
}
methodB {
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
db.commit(); // commit does not occure
}
catch (Exception any) {
db.rollback(); // transaction is marked for rollback
}
// end of connection scope, connection is not closed when we leave here
});
}
In the above scenario, both commit and close are handled in methodA. The advantage of writing this way is that if methodB is shared and called without a Session, methodB will create a new Session and commit and close will be handled correctly in methodB.
In nested method calls where inner methods usually accesses the same thread-bound database connection, the shared connection remains available throughout the entire call chain. This means any method in the hierarchy can interact with the already-bound Db session without needing to explicitly pass or create a new connection.
In many business scenarios, you may need to control the commit order of operations across multiple method calls, even when nested. To achieve this, EzQu provides the SessionOptions.COMMIT option and the applyScope(boolean externalCommit) method for defining transaction scopes.
When the SCOPED option is active and COMMIT is not set in the outermost call, the connection stays open and uncommitted until the outer block ends, letting you delay the final commit until all nested work finishes.
If a nested method uses COMMIT or calls session.applyScope(false), although highly uncommon and not recommended, it will override the outer behavior from that point onward. To revert, call session.applyScope(true).
For fine-grained control, set COMMIT in the outermost method and invoke commit() anywhere in the flow; all prior operations are then flushed to the database.
applyScope(true)defers commits, ensuring changes are not finalized until scope is reset.applyScope(false)allows commit to be satisfied at the point of commit!
When SCOPED is enabled on the outermost method, the session closes only when that method terminates.
This approach gives you granular control over transaction boundaries, enabling you to sequence operations precisely while preventing premature commits in complex, nested workflows.
Following are a more examples:
methodA {
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
methodB() // call methodB
// do some more work
db.commit(); // commit occurs only here
}
catch (Exception any) {
db.rollback(); // rollback occurs only here
}
// end of new connection scope, connection is closed when we leave here
}, SessionOptions.SCOPED, SessionOptions.COMMIT);
}
methodB {
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
db.commit(); // commit occurs here for everything up to this point
}
catch (Exception any) {
db.rollback(); // transaction will rollback
}
// end of connection scope, connection is not closed when we leave here
});
}
methodA {
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
methodB() // call methodB
// do some more work
db.commit(); // commit occurs here
}
catch (Exception any) {
db.rollback(); // rollback occurs here
}
// end of new connection scope, connection is closed when we leave here
}, SessionOptions.SCOPED);
}
methodB {
sessionFactory.runInSession(db -> {
// start of connection scope, using a new connection
try {
// do some db work, all is committed automatically
db.commit(); // commit occurs here for everything up to this point
}
catch (Exception any) {
db.rollback(); // the whole transaction will rollback
}
finally {
db.applyScope(true); // return things to out control
}
// end of connection scope, connection is not closed when we leave here
}, SessionOptions.COMMIT);
}
Troubleshooting scope
Handling scope can be tricky and lead to unintended commits. To debug these issues, review the following table.
| Outer Method options | Outer state | Inner Method options | Inner State | Issues |
|---|---|---|---|---|
| SCOPE & COMMIT | Scoped / commit handled locally | SCOPE & COMMIT | NO CHANGE | |
| SCOPE | NO CHANGE | |||
| COMMIT | NO CHANGE | |||
| SCOPE | Scoped / commit handled in outer scope | SCOPE & COMMIT | Scoped / Commit handled locally | remember to reset by calling applyScope(true) |
| SCOPE or NONE | NO CHANGE | |||
| COMMIT | Scoped / Commit handled locally | remember to reset by calling applyScope(true) | ||
| NONE or COMMIT | No scope / commit handled locally | SCOPE & COMMIT | Scoped / Commit handled locally | leaving here will close the connection thus causing a problem in the outer method. |
| SCOPE | Scoped / commit handled here for all next. | leaving here will close the connection thus causing a problem in the outer method. | ||
| COMMIT or NONE | No scope / Commit handled locally | leaving here will close the connection thus causing a problem in the outer method. | ||
| ANY | NEW | can also have: SCOPE with or without COMMIT | Will start a new connection for the inner method not effecting the outer at all. |