Hello Community,
I have a question if its possible to have database transaction scope using ESQ instead of direct SQL. From the documentation it is suggested that we as Devs should use ESQ most of the times because of validation etc.
I have a case where I have to modify two entities and for that I need two separate ESQ (maybe there is a better way). My goal is to wrap that in a single transaction so that all changes are made or none (to avoid inconsistency).
Is this code below valid for this case? Is going to work?
```
using System;
using Terrasoft.Core.DB;
using Terrasoft.Core.Entities;
using (DBExecutor dbExecutor = UserConnection.EnsureDBConnection())
{
dbExecutor.StartTransaction();
try
{
var schemaManager = UserConnection.EntitySchemaManager;
// Contact
Entity contact = schemaManager.GetInstanceByName("Contact").CreateEntity(UserConnection);
contact.SetDefColumnValues();
contact.SetColumnValue("Name", "Jane Doe");
contact.Save();
// Case, linked to the new contact
Entity supportCase = schemaManager.GetInstanceByName("Case").CreateEntity(UserConnection);
supportCase.SetDefColumnValues();
supportCase.SetColumnValue("Subject", "Test case");
supportCase.SetColumnValue("ContactId", contact.PrimaryColumnValue);
supportCase.Save();
// ...more entities as needed
dbExecutor.CommitTransaction();
}
catch
{
dbExecutor.RollbackTransaction();
throw;
}
}
```
Thanks for all answers.
Like
Hi,
Yes, this pattern works, and Creatio’s own core code uses it (sync, process completion, AppManager). Using Entity here is correct. ESQ is mainly for reading data; Entity, or Insert/Update queries, are the right tools for writing.
Why it works
UserConnection.EnsureDBConnection()doesn’t open a new connection each time. It returns theDBExecutorcached on thatUserConnectionand adds one to a reference count.Entity.Save()callsUserConnection.EnsureDBConnection()internally, so it gets the same executor. Every command that executor runs is enlisted in its open transaction.- That covers more than the main INSERT/UPDATE: default record rights, owner rights, localization rows, change tracking, and DB writes made by synchronous event handlers on the same
UserConnection. Save()doesn’t start its own transaction on insert or update.Delete()does, but transactions nest with a depth counter. An inner Commit or Rollback does nothing, so only your outermost call decides.- If the
usingblock ends without a commit (for example, an unexpected exception),Dispose()rolls the transaction back. Your explicitRollbackTransaction()is still good practice.
Things to watch for
Save()can returnfalsewithout throwing. This happens when anOnSaving,OnInsertingorOnUpdatinghandler cancels the save. Yourtryblock would then commit the other entities. Check the return value.- Use the same
UserConnectionfor everything. Work done throughSystemUserConnectionor another connection runs on a different DB connection. It is outside your transaction, and it can block on rows you have locked. -
Some side effects are not rolled back. They are sent during
Save(), not after commit:- async entity event operations (when async execution of entity events is enabled)
- business processes started by a record signal and run in background mode
- websocket and cache notifications
- file content kept in external file storage
Background work may run before your commit, so it won’t see the new record yet. It may also run after a rollback, against a record that no longer exists.
- Keep the work synchronous. Don’t use
async/awaitor extra threads inside the block. - Don’t reuse entity objects after a rollback. They are already marked as saved in memory, so build new instances.
- If you’re inside someone else’s transaction, rethrow the exception. Your inner Rollback does nothing in that case. If you swallow the exception, the outer commit still saves everything.
Suggested version
using (DBExecutor dbExecutor = UserConnection.EnsureDBConnection())
{
dbExecutor.StartTransaction();
try
{
var schemaManager = UserConnection.EntitySchemaManager;
Entity contact = schemaManager.GetInstanceByName("Contact").CreateEntity(UserConnection);
contact.SetDefColumnValues();
contact.SetColumnValue("Name", "Jane Doe");
if (!contact.Save())
throw new InvalidOperationException("Contact was not saved.");
Entity supportCase = schemaManager.GetInstanceByName("Case").CreateEntity(UserConnection);
supportCase.SetDefColumnValues();
supportCase.SetColumnValue("Subject", "Test case");
supportCase.SetColumnValue("ContactId", contact.PrimaryColumnValue);
if (!supportCase.Save())
throw new InvalidOperationException("Case was not saved.");
dbExecutor.CommitTransaction();
}
catch
{
dbExecutor.RollbackTransaction();
throw;
}
}Hi,
Yes, this pattern works, and Creatio’s own core code uses it (sync, process completion, AppManager). Using Entity here is correct. ESQ is mainly for reading data; Entity, or Insert/Update queries, are the right tools for writing.
Why it works
UserConnection.EnsureDBConnection()doesn’t open a new connection each time. It returns theDBExecutorcached on thatUserConnectionand adds one to a reference count.Entity.Save()callsUserConnection.EnsureDBConnection()internally, so it gets the same executor. Every command that executor runs is enlisted in its open transaction.- That covers more than the main INSERT/UPDATE: default record rights, owner rights, localization rows, change tracking, and DB writes made by synchronous event handlers on the same
UserConnection. Save()doesn’t start its own transaction on insert or update.Delete()does, but transactions nest with a depth counter. An inner Commit or Rollback does nothing, so only your outermost call decides.- If the
usingblock ends without a commit (for example, an unexpected exception),Dispose()rolls the transaction back. Your explicitRollbackTransaction()is still good practice.
Things to watch for
Save()can returnfalsewithout throwing. This happens when anOnSaving,OnInsertingorOnUpdatinghandler cancels the save. Yourtryblock would then commit the other entities. Check the return value.- Use the same
UserConnectionfor everything. Work done throughSystemUserConnectionor another connection runs on a different DB connection. It is outside your transaction, and it can block on rows you have locked. -
Some side effects are not rolled back. They are sent during
Save(), not after commit:- async entity event operations (when async execution of entity events is enabled)
- business processes started by a record signal and run in background mode
- websocket and cache notifications
- file content kept in external file storage
Background work may run before your commit, so it won’t see the new record yet. It may also run after a rollback, against a record that no longer exists.
- Keep the work synchronous. Don’t use
async/awaitor extra threads inside the block. - Don’t reuse entity objects after a rollback. They are already marked as saved in memory, so build new instances.
- If you’re inside someone else’s transaction, rethrow the exception. Your inner Rollback does nothing in that case. If you swallow the exception, the outer commit still saves everything.
Suggested version
using (DBExecutor dbExecutor = UserConnection.EnsureDBConnection())
{
dbExecutor.StartTransaction();
try
{
var schemaManager = UserConnection.EntitySchemaManager;
Entity contact = schemaManager.GetInstanceByName("Contact").CreateEntity(UserConnection);
contact.SetDefColumnValues();
contact.SetColumnValue("Name", "Jane Doe");
if (!contact.Save())
throw new InvalidOperationException("Contact was not saved.");
Entity supportCase = schemaManager.GetInstanceByName("Case").CreateEntity(UserConnection);
supportCase.SetDefColumnValues();
supportCase.SetColumnValue("Subject", "Test case");
supportCase.SetColumnValue("ContactId", contact.PrimaryColumnValue);
if (!supportCase.Save())
throw new InvalidOperationException("Case was not saved.");
dbExecutor.CommitTransaction();
}
catch
{
dbExecutor.RollbackTransaction();
throw;
}
}