Why InputStream Doesn't Need Manual Closure When Using SqlSessionFactoryBuilder?
Consider this typical usage pattern:
public class StudentRepositoryImpl implements IStudentRepository {
private SqlSession session;
public void addStudent(Student student) {
try {
InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);
session = factory.openSession();
session.insert("insertStudentRecord", student);
session.commit();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (session != null) {
session.close();
}
}
}
}
When using Resources.getResourceAsStream("mybatis-config.xml"), manual stream closure isn't required. Examining the source code reveals why.
Looking at the SqlSessionFactoryBuilder().build() method:
public SqlSessionFactory build(InputStream inputStream) {
return this.build(inputStream, null, null);
}
The underlying method includes a finally block that ensures stream closure regardless of execution path:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory result;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
result = this.build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Silent failure
}
}
return result;
}
How SqlSession Objects Are Created
The core creation happens when calling openSession() on SqlSessionFactory:
session = factory.openSession();
Since SqlSessionFactory is an interface, we need to examine its implementations. The primary implementation is DefaultSqlSessionFactory.
The openSession method delegates to a helper method:
public SqlSession openSession() {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), null, false);
}
There's also an overloaded version that accepts auto-commit configuration:
public SqlSession openSession(boolean autoCommit) {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), null, autoCommit);
}
The actual session creation occurs in openSessionFromDataSource():
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession result;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
result = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception e) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
return result;
}
The DefaultSqlSession constructor initializes various fields including a modified flag:
public DefaultSqlSession(Configuration config, Executor exec, boolean autoCommit) {
this.configuration = config;
this.executor = exec;
this.modified = false; // Tracks whether data has been modified
this.autoCommit = autoCommit;
}
The modified flag indicates whether in-memory data differs from database data.
How Insert, Update, and Delete Operations Execute
Consider the insertion call:
session.insert("insertStudentRecord", student);
Examining DefaultSqlSession's implementation shows that insert ultimately calls update:
public int insert(String statement, Object parameter) {
return this.update(statement, parameter);
}
public int update(String statement) {
return this.update(statement, null);
}
The update method marks data as modified and delegates to the executor:
public int update(String statement, Object parameter) {
int rowsAffected;
try {
this.modified = true; // Mark that data will be changed
MappedStatement mappedStmt = this.configuration.getMappedStatement(statement);
rowsAffected = this.executor.update(mappedStmt, this.wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
return rowsAffected;
}
Why SqlSession.commit() Commits Transactions
The commit operation in DefaultSqlSession calls an overloaded version:
public void commit() {
this.commit(false);
}
The overloaded method checks commit requirements and updates the modified flag:
public void commit(boolean force) {
try {
this.executor.commit(this.shouldCommitOrRollback(force));
this.modified = false; // Data consistency restored
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
The decision logic is in shouldCommitOrRollback():
private boolean shouldCommitOrRollback(boolean force) {
return !this.autoCommit && this.modified || force;
}
The executor's commit method handles the actual transaction commit:
public void commit(boolean required) throws SQLException {
if (this.closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
} else {
this.clearLocalCache();
this.flushStatements();
if (required) {
this.transaction.commit();
}
}
}
Why Closing SqlSession Doesn't Require Rollback After Commit
When closing the session, the system determines whether rollback is necessary:
public void close() {
try {
this.executor.close(this.shouldCommitOrRollback(false));
this.modified = false;
} finally {
ErrorContext.instance().reset();
}
}
After a successful commit, modified becomes false. The shouldCommitOrRollback(false) evaluation returns false since both !this.autoCommit && this.modified and force are false.
The executor's close method conditionally performs rollback:
public void close(boolean forceRollback) {
try {
try {
this.rollback(forceRollback);
} finally {
if (this.transaction != null) {
this.transaction.close();
}
}
} catch (SQLException e) {
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
this.transaction = null;
this.deferredLoads = null;
this.localCache = null;
this.localOutputParameterCache = null;
this.closed = true;
}
}
The rollback method only executes when required:
public void rollback(boolean required) throws SQLException {
if (!this.closed) {
try {
this.clearLocalCache();
this.flushStatements(true);
} finally {
if (required) {
this.transaction.rollback();
}
}
}
}
If operations execute without explicit commit, modified remains true, triggering rollback during closure since !this.autoCommit && this.modified evaluates to true.