Транзакционный метод в Scala Play with Slick (может быть, похожий на Spring @Transactional?)

Я знаю, что scala как функциональный язык должен работать иначе, чем обычный объектно-ориентированный язык, такой как Java, но я уверен, что должен быть способ обернуть группу изменений базы данных в одну транзакцию, гарантируя атомарность, поскольку а также любое другое свойство ACID.

Как поясняется в справочной документации (http://slick.lightbend.com/doc/3.1.0/dbio.html), DBIOAction позволяет группировать операции БД в транзакции следующим образом:

val a = (for {
  ns <- coffees.filter(_.name.startsWith("ESPRESSO")).map(_.name).result
  _ <- DBIO.seq(ns.map(n => coffees.filter(_.name === n).delete): _*)
} yield ()).transactionally

val f: Future[Unit] = db.run(a)

Тем не менее, мой вариант использования (и большинство реальных примеров, которые я могу придумать), у меня есть структура кода с контроллером, который предоставляет код для моей конечной точки REST, этот контроллер вызывает несколько служб, и каждая служба делегирует операции с базой данных DAO.

Грубый пример моей обычной структуры кода:

class UserController @Inject(userService: UserService) {
  def register(userData: UserData) = {
    userService.save(userData).map(result => Ok(result))
  }
}

class UserService @Inject(userDao: UserDao, addressDao: AddressDao) {
  def save(userData: UserData) = {
    for {
      savedUser <- userDao.save(userData.toUser)
      savedAddress <- addressDao.save(userData.addressData.toAddress)
    } yield savedUser.copy(address = savedAddress)
  }
}

class SlickUserDao {
  def save(user: User) = {
    db.run((UserSchema.users returning UserSchema.users)).insertOrUpdate(user)
  }
}

Это простой пример, но большинство из них имеют более сложную бизнес-логику на уровне службы.

Я не хочу:

  1. Мои DAO должны иметь бизнес-логику и решать, какие операции с базой данных выполнять.
  2. Верните DBAction из моих DAO и выставьте классы постоянства. Это полностью противоречит цели использования DAO и значительно усложняет дальнейший рефакторинг.

Но я определенно хочу транзакцию вокруг всего моего контроллера, чтобы гарантировать, что в случае сбоя какого-либо кода все изменения, сделанные при выполнении этого метода, будут отменены.

Как я могу реализовать полную транзакцию контроллера с помощью Slick в приложении Scala Play? Я не могу найти документацию о том, как это сделать.

Кроме того, как я могу отключить автоматическую фиксацию в slick? Я уверен, что есть способ, и я просто что-то упускаю.

РЕДАКТИРОВАТЬ:

Поэтому, прочитав об этом немного больше, я чувствую, что теперь лучше понимаю, как slick использует соединения с базой данных и сеансами. Это очень помогло: http://tastefulcode.com/2015/03/19/modern-database-access-scala-slick/.

То, что я делаю, - это случай создания в будущем, и, основываясь на этой статье, нет возможности использовать одно и то же соединение и сеанс для нескольких операций такого рода.

Проблема в том, что я действительно не могу использовать никакую другую композицию. У меня есть значительная бизнес-логика, которую необходимо выполнять между запросами.

Думаю, я могу изменить свой код, чтобы позволить мне использовать композицию действий, но, как я уже упоминал ранее, это вынуждает меня кодировать мою бизнес-логику с учетом таких аспектов, как транзакционность. Этого не должно быть. Это загрязняет бизнес-код и значительно усложняет написание тестов.

Любое обходное решение этой проблемы? Какой-нибудь проект git, который разбирает это, что я пропустил? Или, что более радикально, любой другой фреймворк персистентности, поддерживающий это? Из того, что я читал, Anorm прекрасно поддерживает это, но я могу неправильно понять это и не хочу менять фреймворк, чтобы узнать, что это не так (как это произошло со Slick).


person redwulf    schedule 06.07.2016    source источник


Ответы (1)


В slick нет таких вещей, как транзакционные аннотации или тому подобное. Ваше второе «не хочу» на самом деле правильный путь. Вполне разумно возвращать DBIO[User] из вашего DAO, что никоим образом не противоречит их цели. Так работает слик.

class UserController @Inject(userService: UserService) {
  def register(userData: UserData) = {
    userService.save(userData).map(result => Ok(result))
  }
}

class UserService @Inject(userDao: UserDao, addressDao: AddressDao) {
  def save(userData: UserData): Future[User] = {
    val action = (for {
      savedUser <- userDao.save(userData.toUser)
      savedAddress <- addressDao.save(userData.addressData.toAddress)
      whatever <- DBIO.successful(nonDbStuff)
    } yield (savedUser, savedAddress)).transactionally

    db.run(action).map(result => result._1.copy(result._2))
  }
}

class SlickUserDao {
  def save(user: User): DBIO[User] = {
    (UserSchema.users returning UserSchema.users).insertOrUpdate(user)
  }
}
  • Подпись save в вашем классе обслуживания осталась прежней.
  • В контроллерах нет материалов, связанных с БД.
  • Вы имеете полный контроль над транзакциями.
  • Я не могу найти случая, когда приведенный выше код сложнее поддерживать/рефакторить по сравнению с вашим исходным примером.

Существует также довольно исчерпывающая дискуссия, которая может быть вам интересна. См. Slick 3.0 с блоками транзакций необходимы для взаимодействия с библиотеками.

person Roman    schedule 06.07.2016
comment
Что ж, на самом деле я могу найти в этом несколько неправильных вещей: делает мои сервисы явно зависимыми от гладких классов, поэтому нет смысла иметь DAO (на уровне БД абстракция невозможна). Это означает, что моя бизнес-логика и доступ к базе данных находятся на одном уровне. Делает это действительно трудно тестировать и издеваться. Кроме того, если я хочу изменить свою реализацию доступа к базе данных, мне нужно будет изменить свои службы, поэтому любой рефакторинг будет болезненным. Это также загрязняет мой бизнес-код сквозными проблемами, такими как ограничение транзакции, что, опять же, вероятно, усложнит тестирование. Не может быть единственным способом сделать это. Спасибо в любом случае - person redwulf; 06.07.2016
comment
Я согласен с тем, что желательно избегать явной зависимости от slick, но, насколько я знаю, нет другого решения ATM, если вы хотите работать с транзакциями. Я не говорю, что это окончательное решение, на самом деле я был бы более чем счастлив, если бы кто-то доказал, что я ошибаюсь. Что касается изменения структуры постоянства: поскольку slick не похож на ваш обычный ORM, он, вероятно, в любом случае будет PITA. И, честно говоря, сколько раз вы когда-либо меняли фреймворки постоянства в работающей системе? :) В любом случае, я буду следить за этой темой, может быть, кто-то предложит более приличное решение. - person Roman; 06.07.2016
comment
Эхех, я совсем недавно изменил структуру сохраняемости с Mongo и ReactiveMongo на Postgres и Slick (не спрашивайте...), и я уверен, что достаточно скоро нам придется частично изменить ее обратно на Mongo или другую базу данных документов. Я как бы пытаюсь избежать еще одного огромного рефакторинга, КОГДА это происходит. Транзакции не будут иметь большого значения для большей части этого второго рефакторинга, но сама работа рефакторинга, безусловно, будет иметь значение. Написание модульных тестов также кажется немного сложнее. В любом случае, будем надеяться, что у кого-то есть альтернативный ответ. - person redwulf; 06.07.2016
comment
После долгих исследований и неудачных попыток кажется, что просто невозможно полностью изолировать уровень персистентности в пятне 3, как я описал, так что вы правы, когда говорите, что это правильный путь в пятне. Я все еще чувствую, что это ужасная архитектура, и я, вероятно, никогда больше не буду использовать Slick в другом проекте, пока не увижу, что это ограничение устранено. Тем не менее, я должен присудить награду, и, несмотря на то, что ваш ответ НЕ является РЕШЕНИЕМ моей проблемы, он ПРАВИЛЬЕН и может помешать кому-то другому тратить много времени на попытки сделать что-то, что просто невозможно. - person redwulf; 18.07.2016
comment
Также стоит отметить, что вы сможете координировать действия нескольких вызовов DAO из одного метода сервисного уровня, однако на прикладном уровне вы, как правило, координируете несколько сервисных методов для выполнения задачи. Как можно обеспечить участие всех этих методов службы в общей транзакции? Вы не хотите возвращать типы Slick из своего сервисного уровня... - person ThaDon; 29.03.2017
comment
у меня пример работает только в том случае, если я также делаю следующие импорты в DAO и в службе: protected val dbConfig = dbConfigProvider.get[JdbcProfile] import dbConfig._ import profile.api._ Это не проблема, что DAO и служба используют собственный dbConfigProvider? Если служба не содержит этих импортов, произойдет ошибка утечки из-за того, что DAO возвращает объект DBIO. - person rnd; 18.10.2020