#はじめに
DB操作を行う際、ReadのみかWriteを伴うかに応じて処理を分けたい場合があります。これらをコンパイル時に判定できれば、Masterに問い合わせるべきクエリをSlaveに発行してしまうような事故を未然に防ぐことができます。
具体的には、以下の機能が欲しいです。
- Read/Writeを型で表現する
- ReadとWriteを合成するとWriteになる
このような合成可能トランザクションモナドとしてはFujitaskが有名ですが、本記事ではatnos-effを使ったトランザクションEffを考案します。
#作用の変換
ReadとWriteを区別するため、次のように型を定義します。
// 使っているライブラリのものに置き換える
type DBIO[A] = MyDBIO[A]
case class DBIORead[A](value: DBIO[A])
case class DBIOWrite[A](value: DBIO[A])
type _dbior[R] = DBIORead |= R
type _dbiow[R] = DBIOWrite |= R
Effを利用する側のコードは以下のようになると思います。
def runReadWriteSample: Option[User] = ({
type R = Fx.fx2[DBIORead, DBIOWrite]
for {
_ <- fromDBIOWrite(AddUser(User("hoge", "hoge fuga", 20)))
userOption <- fromDBIORead(FindUserById("hoge"))
} yield userOption
}).runDBIO.runTransactionDBIO.run
しかしながら、このようなコードではReadトランザクションとWriteトランザクションをばらばらに実行することになり、期待した挙動にはなりません。
そこで、作用DBIOReadを作用DBIOWriteに変換することを考えます。atnos-effでは、次のように記述することで作用を変換できます。
implicit def fromDBIOReadToWrite: DBIORead ~> DBIOWrite =
new (DBIORead ~> DBIOWrite) {
override def apply[X](x: DBIORead[X]): DBIOWrite[X] =
DBIOWrite(x.value)
}
implicit def deriveDBIOMember[R](
implicit member: MemberIn[DBIOWrite, R]
): MemberIn[DBIORead, R] = member.transform[DBIORead]
こうした変換コードを記述することで、ReadとWriteを合成して1つの作用として振る舞います。
def runReadWriteSample: Option[User] = ({
type R = Fx.fx1[DBIOWrite]
for {
_ <- fromDBIOWrite[R, Unit](AddUser(User("hoge", "hoge fuga", 20)))
userOption <- fromDBIORead[R, Option[User]](FindUserById("hoge")) // 同じfor内でread/writeを合成してwriteになっている
} yield userOption
}).runTransactionDBIO.run
ここで仮にtype R = Fx.fx1[DBIORead]
と書くとコンパイルエラーになります。また、この場合はfromDBIOWrite等の型引数を省略して自動的にDBIOWriteに推論することもできます。ただし、ReadOnlyな場合にtype R = Fx.fx1[DBIOWrite]
と書いてしまうと予期せずDBIOWriteに変換されるので注意が必要です。
#その他の応用
作用の変換はとても強力な機能で、他にも様々な応用が考えられます。例えば、Aの権限が必要が必要な操作 + Bの権限が必要な操作 = A+Bの権限が必要な操作
のようなルールの権限Effは、HListと組み合わせることで次のような変換コードにより実現できます。
implicit def fromAuthZHNil: AuthZ[HNil, *] ~> AuthZ[P, *] =
new (AuthZ[HNil, *] ~> AuthZ[P, *]) {
override def apply[X](fa: AuthZ[HNil, X]): AuthZ[P, X] = fa match {
case Authorize(userId, scope) =>
Authorize[P](userId, scope)
}
}
implicit def fromAuthZHCons[H, T <: HList](
implicit selector: Selector[P, H],
tail: AuthZ[T, *] ~> AuthZ[P, *]
): AuthZ[H :: T, *] ~> AuthZ[P, *] = new (AuthZ[H :: T, *] ~> AuthZ[P, *]) {
override def apply[X](fa: AuthZ[H :: T, X]): AuthZ[P, X] = fa match {
case a: Authorize[H :: T] =>
Authorize[P](a.userId, a.scope)
}
}
implicit def deriveAuthZMember[T[_], R](
implicit member: MemberIn[AuthZ[P, *], R],
t: T ~> AuthZ[P, *]
): MemberIn[T, R] = member.transform[T]
#サンプルコード
transaction-eff-sample