Соединения и транзакции

Соединениями внутри ServerContext управляет ConnectionManager. Они открываются лениво при первом обращении и кэшируются по строковому идентификатору datasource.

Для основной БД ERP зарегистрированы:

Константа Идентификатор Назначение

ERP_MASTER

erp-master

Записи и чтение, которому нужна актуальность относительно текущей транзакции

ERP_SLAVE

erp-slave

Чтение с реплики, если допустима задержка репликации

Оба соединения получают 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 после каждого успешно завершённого сервисного вызова:

  1. вызывает context.commit();

  2. вызывает context.recycle();

  3. закрывает использованные 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:

Адаптер Поведение

ServerContextConnectionSet

Делегирует master, slave, commit и rollback текущему ServerContext; recycle() и setAutoCommit(…​) ничего не делают

SingleConnectionConnectionSet

Оборачивает одно переданное соединение; slave совпадает с master; адаптер не владеет соединением и не закрывает его при recycle

DefaultConnectionSet

Старый самостоятельный владелец соединений; не используйте его для нового кода, если доступен ServerContext

Передача ServerContextConnectionSet в старый DAO сохраняет общие соединения контекста. Создание отдельного DefaultConnectionSet внутри сервисной операции отделяет изменения от транзакции контекста и обычно является ошибкой.

Практические правила

  • Держите транзакцию короткой; не выполняйте долгий сетевой вызов между изменением БД и commit.

  • Не вызывайте commit() внутри repository: границей управляет владелец ServerContext.

  • После ошибки SQL не продолжайте записи в том же соединении без осознанного rollback.

  • Не закрывайте вручную соединение, полученное у контекста: его жизненным циклом управляет ConnectionManager.

  • Не смешивайте сервисные операции и соединение, открытое вне контекста, если они должны быть атомарны.

  • Учитывайте, что несколько datasource не образуют общую атомарную транзакцию.