4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DDDでslickのトランザクションを扱う

Last updated at Posted at 2020-08-28

前置き

ScalaでメジャーなDBアクセスライブラリのslickを使用する環境で、ドメイン層とアプリケーション層でいかにしてトランザクションをあつかうかについてのメモです。

それぞれのトランザクション

DDDでのトランザクション

複数の集約にまたがってアトミックな処理が求められる場合にはレポジトリ内でセッションが完結しません。DDDの定石ではアプリケーション層でトランザクションを管理することになります。

このとき広く使われているのは、スレッドローカルなトランザクションやセッションを引数で引き回して実現する方法です。

// アプリケーション層
DB.transaction { session =>
  domainServiceA.do(a, session)
  domainServiceB.do(b, session)
}

slickのトランザクション

次にslickのトランザクションの解説です。

細かいことは slick-doc-ja 3.0 — データベースI/Oアクション を見ていただきたいですが、クエリーはその場で実行されるのではなく組み立てられたアクションを返すようになっており、それらを合成した一つのアクションとしてまとめて実行することでトランザクションを利用できます。

val action = for {
  _ <- domainServiceA.do(a)
  - <- domainServiceB.do(b)
} yield ()
DB.run(action)

slickを使ってアプリケーション層でトランザクションをあつかう場合、レポジトリやドメインサービスでslickのアクションを返すようにする必要が出てきます。

ドメイン層でslickアクションを隠蔽する

インフラ層での実装手段であるslickの型がドメイン層に出てくる、つまりドメイン層がslickに依存するのは好ましくありません。

ドメイン層でslickアクションを隠蔽したいのでそうしましょう。やり方を考えていきます。

ラッパーを使う

おそらく多くの言語で使える素直な方法です。以下のような形でしょうか。

// ドメイン層
trait Action[R] {
  ...
}
class DomainServiceA(repository: RepositoryA) {
  def do(a: A): Action[Unit] = ...
}
class DomainServiceB // 省略

// インフラ層
class SlickAction[R](val underlying: DBIOAction[R, NoStream, Effect.All]) extends Action[R] {
  ...
}
class RepositoryAImpl {
  def find(id: Id): Action[A] = {
    SlickAction(Table.A.filter(...).result.headOption.map { ... })
  }
}

// アプリケーション層
val action = for {
  _ <- domainServiceA.do(a)
  - <- domainServiceB.do(b)
} yield ()
DB.run(action.asInstanceOf[SlickAction[Unit]].underlying)

げぇ! asInstanceOf !!!

もちろん match を使っても良いですし、具体的な型に依存した処理を書く場所もインフラ層にやらせてもいいかもしれません。しかし、望んだ型でなかったときは実行時例外を投げるしかありません。良いやり方とは言えないようです。

型引数と型クラスを使う

そもそもやりたいのはドメイン層でアクションの具体的な型に触れたくないだけです。そこでアクションを型引数にしてしまいます。また型引数ならわざわざラッパーを使わずとも型クラスで扱うことができます。だいたい以下のようになります。

// ドメイン層
trait IO[F[_]] {
  // 型クラス
  // map, flatMap, filter, withFilter やその他必要な操作を抽象メソッドを定義
}
class DomainServiceA[F[_]](repository: RepositoryA[F])(implicit F: IO[F]) {
  def do[R](a: A): F[R] = ...
}
class DomainServiceB // 省略

// インフラ層
package object infra {
  type SlickIOAction[+R] = DBIOAction[R, NoStream, Effect.All]
  implicit object SlickIO extends IO[SlickIOAction] {
    // 型クラスのインスタンス
    // map, flatMap, filter, withFilter などを実装する
  }
}

class RepositoryAImpl extends RepositoryA[SlickIOAction] {
  ...
}

// アプリケーション層
val action = for {
  _ <- domainServiceA.do(a)
  - <- domainServiceB.do(b)
} yield ()
DB.run(action)

型引数と型クラスによりドメイン層ではインフラ層での具体的な型に触れずにすみ、具体的な型を知っているアプリケーション層ではキャストなしに触ることができます。

なお、DBIOAction は概ね scala.concurrent.Future と同じ操作をもっているので Future の用の型クラスのインスタンスを用意すれば、ドメイン層のテストでサードパーティライブラリに依存せずにすみます。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?