Соединения и транзакции
Соединениями внутри ServerContext управляет ConnectionManager. Они открываются лениво при первом обращении и кэшируются по строковому идентификатору datasource.
Для основной БД ERP зарегистрированы:
| Константа | Идентификатор | Назначение |
|---|---|---|
|
|
Записи и чтение, которому нужна актуальность относительно текущей транзакции |
|
|
Чтение с реплики, если допустима задержка репликации |
Оба соединения получают autoCommit = false. Не открывайте их через DriverManager: используйте контекст, repository или переданный ConnectionSet.
Получение соединения
В repository предпочтительны свойства ErpRepository.master и ErpRepository.slave:
class DefaultExampleRepository(manager: ConnectionManager) : ErpRepository(manager) {
fun updateTitle(id: Int, title: String) {
master.prepareStatement("UPDATE example SET title = ? WHERE id = ?").use { ps ->
ps.setString(1, title)
ps.setInt(2, id)
ps.executeUpdate()
}
}
fun findTitleForReport(id: Int): String? =
slave.prepareStatement("SELECT title FROM example WHERE id = ?").use { ps ->
ps.setInt(1, id)
ps.executeQuery().use { rs -> if (rs.next()) rs.getString(1) else null }
}
}
Инфраструктурный код может получить соединение прямо из контекста:
val con = context.getConnection<Connection>(ERP_MASTER)
При неизвестном идентификаторе ConnectionFactory выбрасывает IllegalArgumentException. После context.close() получение соединения и управление транзакцией завершаются IllegalStateException.
Прикладной код должен предпочитать сервис. Прямое соединение не добавляет доменные проверки и события, а SQL разрешён только в DAO/repository-пакетах.
Транзакционный контекст
В режиме transactional = true один holder каждого запрошенного datasource сохраняется до закрытия контекста. Сервисы и repositories разделяют эти соединения.
withContext(user, transactional = true) {
val service = getService<ProcessService>()
service.updateStatus(processId, statusId)
service.addDescription(processId, message)
} // withContext вызывает commit после успешного блока
При ручном управлении:
val context = GlobalContext.newServerContext(user, transactional = true)
try {
// операции
context.commit()
} catch (e: Exception) {
context.rollback()
throw e
} finally {
context.close()
}
commit() проходит по всем созданным holders. Если один commit завершается ошибкой, исключение передаётся вызывающему коду. Это не распределённая транзакция: если используются несколько datasource, уже подтверждённый holder автоматически не откатывается.
rollback() пытается откатить каждый открытый holder; отдельные ошибки журналируются и не пробрасываются. close() закрывает созданные соединения, но сам код ServerContext не вызывает перед этим rollback().
Нетранзакционный контекст
В режиме transactional = false proxy после каждого успешно завершённого сервисного вызова:
-
вызывает
context.commit(); -
вызывает
context.recycle(); -
закрывает использованные holders и очищает их кэш.
Следующий сервисный вызов откроет новые соединения. Такой режим подходит для независимых операций чтения и коротких операций, которым не нужна общая транзакция.
если реализация сервиса выбросила исключение, текущий proxy передаёт его вызывающему коду до автоматических commit и recycle. Ресурсы гарантированно освобождаются при последующем context.close(); если контекст сохраняется после перехваченной ошибки, вызовите rollback()/recycle() согласно сценарию либо завершите контекст.
|
Прямой вызов context.getConnection(…) не проходит через service proxy. В нетранзакционном контексте автоматический commit/recycle произойдёт только после успешного вызова сервиса или при явном управлении со стороны владельца контекста.
Master и slave
Запись всегда выполняйте через ERP_MASTER. Slave может отставать от master, поэтому чтение с реплики сразу после изменения не гарантирует read-your-writes.
Используйте master для:
-
проверки результата только что выполненной записи;
-
принятия решения, от которого зависит следующая запись текущей операции;
-
блокировок и запросов
SELECT … FOR UPDATE; -
данных, для которых задержка репликации недопустима.
Slave подходит для независимых выборок, отчётов и справочного чтения, где кратковременная задержка приемлема.
Совместимость с ConnectionSet
Часть старого API принимает ru.bgcrm.util.sql.ConnectionSet:
| Адаптер | Поведение |
|---|---|
|
Делегирует master, slave, commit и rollback текущему |
|
Оборачивает одно переданное соединение; slave совпадает с master; адаптер не владеет соединением и не закрывает его при recycle |
|
Старый самостоятельный владелец соединений; не используйте его для нового кода, если доступен |
Передача ServerContextConnectionSet в старый DAO сохраняет общие соединения контекста. Создание отдельного DefaultConnectionSet внутри сервисной операции отделяет изменения от транзакции контекста и обычно является ошибкой.
Практические правила
-
Держите транзакцию короткой; не выполняйте долгий сетевой вызов между изменением БД и commit.
-
Не вызывайте
commit()внутри repository: границей управляет владелецServerContext. -
После ошибки SQL не продолжайте записи в том же соединении без осознанного rollback.
-
Не закрывайте вручную соединение, полученное у контекста: его жизненным циклом управляет
ConnectionManager. -
Не смешивайте сервисные операции и соединение, открытое вне контекста, если они должны быть атомарны.
-
Учитывайте, что несколько datasource не образуют общую атомарную транзакцию.