DDDのリポジトリを実装するのがダルいので、ライブラリ化したというか前から書いていたけど、Redis, Memcachedなどの実装も追加したので、簡単に説明を書いてみる。 プロジェクトなどで自前で実装する際も、参照実装として参考になると思います。Scalaの例だけど、他の言語でも参考にはなるんじゃないかと勝手に想像してます。
https://github.com/j5ik2o/scala-ddd-base
デフォルトで対応している永続化技術は、以下。
- JDBC
- SkinnyORM
- Slick3
- NOSQL
- Memcached
- Redis
- Guava Cache
- Freeモナド
- こちらは永続化そのものは行いません。上記どれかの実装に委譲することになります。
何が楽になるかというと、上記向けのリポジトリの実装があるので、Daoだけ用意すればリポジトリが実装できるようになります。
中核になるトレイト
中核となる抽象トレイトはこれ。実装はついてないです。
https://github.com/j5ik2o/scala-ddd-base/tree/master/core/src/main/scala/com/github/j5ik2o/dddbase
IOするのはAggregateです。リポジトリによっては実装するメソッドが異なるのでトレイトは細かく分かれています。Mは型コンストラクタです。
trait AggregateSingleReader[M[_]] extends AggregateIO[M] { def resolveById(id: IdType): M[AggregateType] }
SkinnyORM向け実装トレイト
一例としてSkinnyORM向けに実装を提供するトレイトの説明を簡単にします。M
はここでは、ReaderT[Task, DBSession, A]
としています。Task
はmonix.eval.Task
です。この型は実装ごとに変わります。たとえば、Redis向けの実装では、ReaderT[Task, RedisConnection, A]
になります。
実装を提供するトレイトはAggregateIOBaseFeature
を継承しますが、ほとんどのロジックでSkinnyDaoSupport#Dao
を実装したオブジェクトに委譲します。つまり、リポジトリを作る場合は、これらのトレイトをリポジトリのクラスにミックスインして、Daoの実装を提供するだけでよいことになります。
object AggregateIOBaseFeature { type RIO[A] = ReaderT[Task, DBSession, A] } trait AggregateIOBaseFeature extends AggregateIO[RIO] { override type IdType <: AggregateLongId type RecordType <: SkinnyDaoSupport#Record type DaoType <: SkinnyDaoSupport#Dao[RecordType] protected val dao: DaoType } trait AggregateSingleReadFeature extends AggregateSingleReader[RIO] with AggregateBaseReadFeature { override def resolveById(id: IdType): RIO[AggregateType] = for { record <- ReaderT[Task, DBSession, RecordType] { implicit dbSession: DBSession => Task { dao.findBy(byCondition(id)).getOrElse(throw AggregateNotFoundException(id)) } } aggregate <- convertToAggregate(record) } yield aggregate }
実装サンプル
実装する際は、Aggregate*Feature
のトレイトをミックスしてください(UserAccountRepositoryは実装を持たない抽象型です)。あとはDaoの実装の提供だけです。以下の例では、UserAccountComponent
がUserAccountRecord
, UserAccountDao
を提供します。
object UserAccountRepository { type BySlick[A] = Task[A] def bySkinny: UserAccountRepository[BySkinny] = new UserAccountRepositoryBySkinny } class UserAccountRepositoryBySkinny extends UserAccountRepository[BySkinny] with AggregateSingleReadFeature with AggregateSingleWriteFeature with AggregateMultiReadFeature with AggregateMultiWriteFeature with AggregateSingleSoftDeleteFeature with AggregateMultiSoftDeleteFeature with UserAccountComponent { override type RecordType = UserAccountRecord override type DaoType = UserAccountDao.type override protected val dao: UserAccountDao.type = UserAccountDao override protected def convertToAggregate: UserAccountRecord => RIO[UserAccount] = { record => ReaderT { _ => // このメソッド内部でDBを利用したり、非同期タスクを実行する可能性もあるので、RIO形式を取っている Task.pure { UserAccount( id = UserAccountId(record.id), status = Status.withName(record.status), emailAddress = EmailAddress(record.email), password = HashedPassword(record.password), firstName = record.firstName, lastName = record.lastName, createdAt = record.createdAt, updatedAt = record.updatedAt ) } } } override protected def convertToRecord: UserAccount => RIO[UserAccountRecord] = { aggregate => ReaderT { _ => Task.pure { UserAccountRecord( id = aggregate.id.value, status = aggregate.status.entryName, email = aggregate.emailAddress.value, password = aggregate.password.value, firstName = aggregate.firstName, lastName = aggregate.lastName, createdAt = aggregate.createdAt, updatedAt = aggregate.updatedAt ) } } } }
DaoはSkinnyDaoSupport#Daoを実装する形式になります。これはSkinnyCRUDMapperの派生型です。 僕の場合は、ここで紹介した方法でスキーマから自動生成しています。
package skinny { import com.github.j5ik2o.dddbase.skinny.SkinnyDaoSupport import scalikejdbc._ import _root_.skinny.orm._ trait UserAccountComponent extends SkinnyDaoSupport { case class UserAccountRecord( id: Long, status: String, email: String, password: String, firstName: String, lastName: String, createdAt: java.time.ZonedDateTime, updatedAt: Option[java.time.ZonedDateTime] ) extends Record object UserAccountDao extends Dao[UserAccountRecord] { override def useAutoIncrementPrimaryKey: Boolean = false override val tableName: String = "user_account" override protected def toNamedValues(record: UserAccountRecord): Seq[(Symbol, Any)] = Seq( 'status -> record.status, 'email -> record.email, 'password -> record.password, 'first_name -> record.firstName, 'last_name -> record.lastName, 'created_at -> record.createdAt, 'updated_at -> record.updatedAt ) override def defaultAlias: Alias[UserAccountRecord] = createAlias("u") override def extract(rs: WrappedResultSet, s: ResultName[UserAccountRecord]): UserAccountRecord = autoConstruct(rs, s) } } }
利用するときは、以下のような感じでリポジトリが返すReaderT[Task, DBSession, UserAccount]#run
にDBSession
を渡すとTask
が返ってきます。
それをrunAsync
するとFuture[UserAccount]
が取得できます。他の実装でもほとんど同じように使えます。ご参考までに。
val resultFuture: Future[UserAccount] = (for { _ <- repository.store(userAccount) r <- repository.resolveById(userAccount.id) } yield r).run(AutoSession).runAsync