FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

研修でscala-dddbaseを使って掲示板作ってみました

はじめまして、新人のno_sugiyamaです。
研修でscala、PlayFramework、ドメイン駆動設計(DDD)を使って掲示板を作りました。
実装する際に使ったscala-dddbaseライブラリがDDDで実装するときに非常に便利だったので、実際にどんな感じで使ったかをご紹介したいと思います。

scala-dddbase

scala-dddbaseは@j5ik2oさんが作ったscalaでDDDを実装するときに使えるライブラリです。主にエンティティとリポジトリに関するトレイトが用意されていて、これらのトレイトをミックスインしていく形で使います。トレイトにはあらかじめ必要なメソッド等が定義されているため、自然にDDDの各要素に必要な機能を取り入れることが出来ます。
早速使っていきます。

エンティティ

まずはエンティティの識別に利用するIDを作っていきます。
今回作った掲示板では投稿をエンティティとしたのでToukouIDとします。

package domain.model.toukou

import org.sisioh.dddbase.core.model.Identifier

case class ToukouID(value: String) extends Identifier[String]

IDを定義するときにはIdentifierトレイトをミックスインします。型パラメータには引数のvalueと同じ型を指定します。
次に投稿を定義していきます。

package domain.model.toukou

import ToukouID
import org.sisioh.dddbase.core.model.Entity

case class Toukou(
    identifier: ToukouID,
    comment: String,
    email: String
    ) extends Entity[ToukouID]

エンティティを定義するときはEntityトレイトをミックスインします。型パラメータにはToukouIDを指定します。
独自にエンティティを定義する場合、エンティティ同士を比較できるようにするためのメソッドを定義する必要がありますが、Entityトレイトですでに定義済みなため不要です。
今回は単純なエンティティでしたが、順序を考慮したい、シリアライズしたいという場合は、専用のトレイトが用意されているのでそちらを継承すれば良いかと思います。
(投稿も表示順序があるから順序を考慮すべきだったかも…)

リポジトリ

エンティティのライフサイクル(CRUD)を司るリポジトリを定義していきます。
まずは、アプリケーション層からセッション情報を渡してトランザクションを張れるようにするためのコンテキストを定義します。

package domain.lifecycle

import org.sisioh.dddbase.core.lifecycle.sync.SyncEntityIOContext

import play.api.Play.current
import play.api.db.DB
import java.sql.Connection

case class SyncEntityIOContextOnJDBC(connection: Connection) extends SyncEntityIOContext

object SyncEntityIOContextOnJDBC {
  def setSession(dbname: String = "default"): SyncEntityIOContext =
   SyncEntityIOContextOnJDBC(DB.getConnection(dbname))
}

scala-dddbaseではリポジトリとコンテキストについて、sync・asyncの2種類のパッケージが用意されています。今回作る掲示板はsyncパッケージ内のSyncEntityIOContextをミックスインしています。
次にリポジトリの実装です。

package domain.lifecycle.toukou

import java.sql.Connection
import domain.lifecycle.SyncEntityIOContextOnJDBC
import domain.model.toukou.{Toukou, ToukouID}
import infrastructure.jdbc.ToukouDAO
import org.sisioh.dddbase.core.lifecycle.EntityIOContext
import org.sisioh.dddbase.core.lifecycle.sync.{SyncResultWithEntity, SyncRepository}
import scala.util.Try

private[toukou] class ToukouRepositoryOnJDBC extends SyncRepository[ToukouID, Toukou] {

  private def withConnection[A](ctx: EntityIOContext[Try])(f: Connection => A): A =
    ctx match {
      case SyncEntityIOContextOnJDBC(connection) => f(connection)
      case _ => throw new Exception
    }

  private def convertToMap(entity: Toukou): Map[String, Any] =
    Map(
      "id" -> entity.identifier.value,
      "comment" -> entity.comment,
      "email" -> entity.email
    )

  private def convertToEntity(rs: Map[String, Any]): Toukou =
    Toukou(
      identifier = ToukouID(rs("id").toString),
      comment = rs("comment").toString,
      email = rs("email").toString
    )

  def store(entity: Toukou)(implicit ctx: Ctx): Try[Result] = withConnection(ctx) { implicit con =>
    existBy(entity.identifier).map { isExist =>
      val result = if (isExist)
        ToukouDAO.update(convertToMap(entity))
      else
        ToukouDAO.insert(convertToMap(entity))
      result.map(_ => SyncResultWithEntity[This, ToukouID, Toukou](this.asInstanceOf[This], entity))
    }.flatten
  }

  def existBy(identifier: ToukouID)(implicit ctx: Ctx): Try[Boolean] = withConnection(ctx){ implicit con =>
    ToukouDAO.exist(identifier.value).map(_ == 1)
  }

  def resolveBy(identifier: ToukouID)(implicit ctx: Ctx): Try[Toukou] = withConnection(ctx) { implicit con =>
    ToukouDAO.select(identifier.value).map(rs =>
      convertToEntity(rs)
    )
  }

  def deleteBy(identifier: ToukouID)(implicit ctx: Ctx): Try[Result] = withConnection(ctx) { implicit con =>
    resolveBy(identifier).map { entity =>
      ToukouDAO.delete(identifier.value).map(_ =>
        SyncResultWithEntity[This, ToukouID, Toukou](this.asInstanceOf[This], entity)
      )
    }.flatten
  }
}

同期リポジトリを定義するときはSyncパッケージ内のSyncRepositoryトレイトをミックスインします。型パラメータにはIDとEntityを指定します。SyncRepositoryトレイトではstore, resolveBy, existBy, deleteByといった4つの抽象メソッドが定義されています。これら4つのメソッドの実装を定義することで、resolveByMultiやstoreMultiといったメソッドを使えるようになります。

注意点として、SyncRepositoryトレイトにはupdateメソッドが用意されていますが、
def update(identifier: ID, entity: E)(implicit ctx: Ctx) = store(entity)
のようにstoreメソッドを使うため、storeメソッド内で追加と更新をできるようにする必要があります。

おわり

コードは@j5ik2oさんのspetstoreをかなり参考にして書きました。
今回紹介したトレイト以外にも非同期リポジトリやonMemoryリポジトリなどもあるので、機会があったらご紹介したいと思います。

参考サイト