かとじゅんの技術日誌

技術の話をするところ

「エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か」の感想

毎日、ドメイン駆動設計というか、設計の話が投稿されると、楽しくなりますね。

さて、今日の話題は、以下です!

エンティティの同一性を表現するためにequalsをオーバーライドすべきか否か

”稀によくあるサンプル”。多分これ僕が書いた事例ですね。ということで、なぜそうしたか、理由など書いておきたいと思います。

なぜこうしたか

equalsの責務は以下のとおりで、オブジェクトが等しいかどうかを示すものです。エンティティの等価判定の基準に、識別子以外の属性は含めていません。 これにはトレードオフがあります。

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Object.html Object#equalsの責務は、「このオブジェクトと他のオブジェクトが等しいかどうかを示します。」

以下のように実装したと場合に、(ID以外の)属性が変わったエンティティを特定することが可能です。

scala> case class EmployeeId(value: Int) extends Identifier
defined class EmployeeId

scala> case class Employee(identifier: EmployeeId, name: String) extends Entity[EmployeeId]
defined class Employee

scala> val list = Seq(Employee(EmployeeId(1), "yamada taro"), Employee(EmployeeId(2), "yamada hanako"))
list: Seq[Employee] = List(Employee(EmployeeId(1),yamada taro), Employee(EmployeeId(2),yamada hanako))

// "yamada hanako"が結婚して名前が変わるイベントが発生、リストの内容も合わせて変更される

scala> list.contains(Employee(EmployeeId(2), "yamada hanako"))
res0: Boolean = true

Entity#sameIdentityAsの場合は、 コレクションのメソッドに組み込むことはできないのでそれなりに実装を工夫する必要がありますね。

論理的等価性検査としてのequals

上記のメリットはよいとして、一体equalsメソッドとして何を求めるているのか。DDDの観点があろうがなかろうか、equalsの契約を逸脱する設計をしないようにすべきと考えます。久しぶりに、Effective Javaを開いてみると、equalsは「論理的等価性」検査の責務を持つというような記述はあるものの、「論理的等価性」が具体的に何かは明記されていませんでした。

ちなみにjava.lang.Objectのデフォルト実装は以下となっています。多くの場合はサブクラスで固有のequalsメソッドとしてオーバーライドされますが、されない場合もあります。java.util.Randomは、同じ乱数列を生成するかで等価判定できたはずですが、クライアントがそれを求めてなかったので、デフォルト実装のままとなっているようです。

public boolean equals(Object obj) {
  return (this == obj);
}

契約プログラミングの観点から

契約プログラミングの観点では、java.lan.Object#equalsのJavadocに書かれていること以上のことは求めてはいけません。つまり、一般契約に従っている以上は「オブジェクトが等しいかどうか」の仕様を満たしているはずです。

DDDの文脈では、等価性は(エンティティが持つ)値がすべて同じであればtrueとみなし、同一性は識別子が同じであればtrueとみなすものと解釈できそうです。つまり、エンティティの定義である「同一性によって定義されるオブジェクト」に照らすと equals をオーバーライドすることは適切ではありません。 この点では、本来オーバーライドすべきは eq と言えるかもしれません。

このアイデアは有益ですしこの設計を取ってもよいと思っていますが、java.lan.Object#equalsの契約に照らして考えるに「適切ではありません」とまでは言い切れないと考えています。

結局どうすべきか

まぁ身の蓋もないですが、こうすべきだと思っています。(ここでオーバーライドするという意味は、IDのみの等価判断する実装としてオーバーライドするかしないかという意味です)

また、equalshashCodeのようにもともと抽象度の高いインターフェイスは意図がわかりにくいことがあります。対策としては、ドキュメンテーションコメントに自然言語できちんと設計の意図を記述するべきで、利用者も求められる契約が何かを理解すべきだと思っています。(自壊の念を込めて)

実装を追わなければ equals がオーバーライドされていることが分からないことは、余分な意識コストとなってしまうでしょう。

akka-httpにswagger2.xを組み込む方法

akka-httpswagger2.xを組み込む方法を以下に示します。

※DIコンテナであるAirframeを使っていますが、もちろん必須ではありません。適宜読み替えてくだだい。

ライブラリの依存関係

swagger-akka-httpを追加します。javax.ws.rs-apiはアノテーションを利用するために追加します。akka-http-corsは必要に応じて追加してください。

libraryDependencies ++= Seq(
"com.typesafe.akka"            %% "akka-http"         % "10.1.5",
"com.github.swagger-akka-http" %% "swagger-akka-http" % "2.0.0"
"javax.ws.rs"                  % "javax.ws.rs-api"    % "2.0.1"
"ch.megard"                    %% "akka-http-cors"    % "0.3.0"
// ...
)

コントローラの例

コントローラに相当するクラスにアクションを作って、そのメソッドにアノテーションを割り当てます。別にコントローラを定義しなくとも、エンドポイントごとにRoute定義が分かれていて、アノテーションが付与できればよいです。

アノテーションの使い方は、Swagger 2.X Annotationsを読んでください。

package spetstore.interface.api.controller

import java.time.ZonedDateTime

import akka.http.scaladsl.server.{Directives, Route}
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.{Content, Schema}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import javax.ws.rs._
import monix.eval.Task
import monix.execution.Scheduler
import org.hashids.Hashids
import org.sisioh.baseunits.scala.money.Money
import spetstore.domain.model.basic.StatusType
import spetstore.domain.model.item._
import spetstore.interface.api.model.{CreateItemRequest, CreateItemResponse, CreateItemResponseBody}
import spetstore.interface.generator.jdbc.ItemIdGeneratorOnJDBC
import spetstore.interface.repository.ItemRepository
import wvlet.airframe._

import scala.concurrent.Future

@Path("/items")
@Consumes(Array("application/json"))
@Produces(Array("application/json"))
trait ItemController extends Directives {

  private val itemRepository: ItemRepository[Task]         = bind[ItemRepository[Task]]

  private val itemIdGeneratorOnJDBC: ItemIdGeneratorOnJDBC = bind[ItemIdGeneratorOnJDBC]

  private val hashids = bind[Hashids]

  def route: Route = create

  private def convertToAggregate(id: ItemId, request: CreateItemRequest): Item = Item(
    id = id,
    status = StatusType.Active,
    name = ItemName(request.name),
    description = request.description.map(ItemDescription),
    categories = Categories(request.categories),
    price = Price(Money.yens(request.price)),
    createdAt = ZonedDateTime.now(),
    updatedAt = None
  )

  @POST
  @Operation(
    summary = "Create item",
    description = "Create Item",
    requestBody =
      new RequestBody(content = Array(new Content(schema = new Schema(implementation = classOf[CreateItemRequest])))),
    responses = Array(
      new ApiResponse(responseCode = "200",
                      description = "Create response",
                      content = Array(new Content(schema = new Schema(implementation = classOf[CreateItemResponse])))),
      new ApiResponse(responseCode = "500", description = "Internal server error")
    )
  )
  def create: Route = path("items") {
    post {
      extractActorSystem { implicit system =>
        implicit val scheduler: Scheduler = Scheduler(system.dispatcher)
        entity(as[CreateItemRequest]) { request =>
          val future: Future[CreateItemResponse] = (for {
            itemId <- itemIdGeneratorOnJDBC.generateId()
            _      <- itemRepository.store(convertToAggregate(itemId, request))
          } yield CreateItemResponse(Right(CreateItemResponseBody(hashids.encode(itemId.value))))).runAsync
          onSuccess(future) { result =>
            complete(result)
          }
        }
      }
    }
  }

  // ...

}

swagger-ui

swagger-uidistsrc/main/resource/swaggerとしてコピーしてください。

SwaggerHttpService

次にSwaggerHttpServiceの実装を用意します。

package spetstore.interface.api

import com.github.swagger.akka.SwaggerHttpService
import com.github.swagger.akka.model.Info

class SwaggerDocService(hostName: String, port: Int, val apiClasses: Set[Class[_]]) extends SwaggerHttpService {
  override val host                = s"127.0.0.1:$port" //the url of your api, not swagger's json endpoint
  override val apiDocsPath         = "api-docs" //where you want the swagger-json endpoint exposed
  override val info                = Info() //provides license and other description details
  override val unwantedDefinitions = Seq("Function1", "Function1RequestContextFutureRouteResult")
}

routeの設定

akka-httpのRouteは以下を参考にしてください。SwaggerDocServiceとコントローラをrouteに加えます。また、CORSが必要なら、"ch.megard" %% "akka-http-cors" % "0.3.0" を使うとよいと思います。

package spetstore.interface.api

import akka.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpResponse }
import akka.http.scaladsl.server.{ Directives, Route, StandardRoute }
import wvlet.airframe._
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._
import spetstore.interface.api.controller.ItemController

trait Routes extends Directives {

  private lazy val itemController    = bind[ItemController]
  private lazy val swaggerDocService = bind[SwaggerDocService]

  private def index(): StandardRoute = complete(
    HttpResponse(
      entity = HttpEntity(
        ContentTypes.`text/plain(UTF-8)`,
        "Wellcome to API"
      )
    )
  )

  def routes: Route = cors() {
    pathEndOrSingleSlash {
      index()
    } ~ path("swagger") {
      getFromResource("swagger/index.html")
    } ~ getFromResourceDirectory("swagger") ~
    swaggerDocService.routes ~ itemController.route
  }

}

ブートストラップ

akka-httpの起動部分のコードです。

package spetstore.interface.api

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.settings.ServerSettings
import akka.stream.ActorMaterializer
import wvlet.airframe._

import scala.concurrent.Future
import scala.util.{ Failure, Success }

trait ApiServer {

  implicit val system           = bind[ActorSystem]
  implicit val materializer     = ActorMaterializer()
  implicit val executionContext = system.dispatcher

  private val routes = bind[Routes].routes

  def start(host: String, port: Int, settings: ServerSettings): Future[ServerBinding] = {
    val bindingFuture = Http().bindAndHandle(handler = routes, interface = host, port = port, settings = settings)
    bindingFuture.onComplete {
      case Success(binding) =>
        system.log.info(s"Server online at http://${binding.localAddress.getHostName}:${binding.localAddress.getPort}/")
      case Failure(ex) =>
        system.log.error(ex, "occurred error")
    }
    sys.addShutdownHook {
      bindingFuture
        .flatMap(_.unbind())
        .onComplete { _ =>
          materializer.shutdown()
          system.terminate()
        }
    }
    bindingFuture
  }

}

アプリケーションのブートストラップ部分です。

package spetstore.api

import akka.actor.ActorSystem
import akka.http.scaladsl.settings.ServerSettings
import monix.eval.Task
import org.hashids.Hashids
import slick.basic.DatabaseConfig
import slick.jdbc.JdbcProfile
import spetstore.domain.model.item.ItemId
import spetstore.interface.api.controller.ItemController
import spetstore.interface.api.{ApiServer, Routes, SwaggerDocService}
import spetstore.interface.generator.IdGenerator
import spetstore.interface.generator.jdbc.ItemIdGeneratorOnJDBC
import spetstore.interface.repository.{ItemRepository, ItemRepositoryBySlick}
import wvlet.airframe._

/**
  * http://127.0.0.1:8080/swagger
  */
object Main {

  def main(args: Array[String]): Unit = {
    val parser = new scopt.OptionParser[AppConfig]("spetstore") {
      opt[String]('h', "host").action((x, c) => c.copy(host = x)).text("host")
      opt[Int]('p', "port").action((x, c) => c.copy(port = x)).text("port")
    }
    val system = ActorSystem("spetstore")
    val dbConfig: DatabaseConfig[JdbcProfile] =
      DatabaseConfig.forConfig[JdbcProfile](path = "spetstore.interface.storage.jdbc", system.settings.config)

    parser.parse(args, AppConfig()) match {
      case Some(config) =>
        val design = newDesign
          .bind[Hashids].toInstance(new Hashids(system.settings.config.getString("spetstore.interface.hashids.salt")))
          .bind[ActorSystem].toInstance(system)
          .bind[JdbcProfile].toInstance(dbConfig.profile)
          .bind[JdbcProfile#Backend#Database].toInstance(dbConfig.db)
          .bind[Routes].toSingleton
          .bind[SwaggerDocService].toInstance(
          new SwaggerDocService(config.host, config.port, Set(classOf[ItemController]))
        )
          .bind[ApiServer].toSingleton
          .bind[ItemRepository[Task]].to[ItemRepositoryBySlick]
          .bind[IdGenerator[ItemId]].to[ItemIdGeneratorOnJDBC]
          .bind[ItemController].toSingleton
        design.withSession { session =>
          val system = session.build[ActorSystem]
          session.build[ApiServer].start(config.host, config.port, settings = ServerSettings(system))
        }
      case None =>
        println(parser.usage)
    }
  }
}

まとめ

最低限、考慮すべきこととしては、akka-httpのroute dslはエンドポイントごとに分割してswaggerアノテーションを割り当てれるようにしてください。これ以外はswaggerの一般的な使い方と変わりません。

Getter/Setterを避けて役に立つドメインオブジェクトを作る

Clean Architecture 達人に学ぶソフトウェアの構造と設計を読んでます。モデリングに関しては成分薄めですが、よい本だと思います。はい。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

本書の大筋から少し逸れるが、「5章 オブジェクト指向プログラミング」の「カプセル化」が面白かったので、これを切り口にモデリングについて考えてみる。

続きを読む

DDDリポジトリを楽に実装するライブラリ

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]としています。Taskmonix.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の実装の提供だけです。以下の例では、UserAccountComponentUserAccountRecord, 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]#runDBSessionを渡すとTaskが返ってきます。 それをrunAsyncするとFuture[UserAccount]が取得できます。他の実装でもほとんど同じように使えます。ご参考までに。

val resultFuture: Future[UserAccount] = (for {
  _ <- repository.store(userAccount)
  r <- repository.resolveById(userAccount.id)
} yield r).run(AutoSession).runAsync

sbtでDAOを自動生成する方法

DDDのリポジトリを実装する際、ほとんどのケースでDAOが必要になります。が、ボイラープレートが多く、自動生成したいところです。というわけで作りました。

どうやって自動化するか

septeni-original/sbt-dao-generator

指定されたスキーマのJDBCメタ情報とテンプレートをマージさせて、ソースコードを出力します。その機能を提供するのがsbt-dao-generator1です。つまり、sbtからコマンド一発でこういうことができるようになるわけですが、 DBのインスタンスが立ち上がっていて、スキーマ情報が組み込まれた状態でないと使えません。

chatwork/sbt-wix-embedded-mysql

sbtから組み込みMySQLを起動するプラグインです。MySQL固定です…。

flyway/flyway-sbt

こちらは言わずもがな、有名なsbtプラグイン。組み込みMySQL上にスキーマを自動作成するために使います。

環境構築手順

実際のサンプルコードは、j5ik2o/scala-ddd-baseをみてください。 flywayを扱うプロジェクトflywayとDAOを自動生成するプロジェクトexampleはわけています。

project/plugins.sbt

プラグインを追加しましょう

addSbtPlugin("com.chatwork" % "sbt-wix-embedded-mysql" % "1.0.9")

addSbtPlugin("jp.co.septeni-original" % "sbt-dao-generator" % "1.0.8")

addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "5.0.0")

テンプレートを作りましょう

FTLでDAOのテンプレートを書きます。以下はSkinnyORMのための例です。レコードクラスとDAOクラスです。

  case class ${className}Record(
<#list primaryKeys as primaryKey>
    ${primaryKey.propertyName}: ${primaryKey.propertyTypeName}<#if primaryKey_has_next>,</#if></#list><#if primaryKeys?has_content>,</#if>
<#list columns as column>
    <#if column.columnName == "status">
        <#assign softDelete=true>
    </#if>
    <#if column.nullable>    ${column.propertyName}: Option[${column.propertyTypeName}]<#if column_has_next>,</#if>
    <#else>    ${column.propertyName}: ${column.propertyTypeName}<#if column_has_next>,</#if>
    </#if>
</#list>
  ) extends Record

  object ${className}Dao extends Dao[${className}Record] {

      override def useAutoIncrementPrimaryKey: Boolean = false

      override val tableName: String = "${tableName}"

      override protected def toNamedValues(record: ${className}Record): Seq[(Symbol, Any)] = Seq(
<#list columns as column>       '${column.name} -> record.${column.propertyName}<#if column.name?ends_with("id") || column.name?ends_with("Id")>.value</#if><#if column_has_next>,</#if>
</#list>
    )

      override def defaultAlias: Alias[UserAccountRecord] = createAlias("${className[0]?lower_case}")

      override def extract(rs: WrappedResultSet, s: ResultName[${className}Record]): ${className}Record = autoConstruct(rs, s)


  }

特定のDAOに依存しないので、ほとんどのものに対応できるはず。以下は、Slick用とSkinny用の両方に対応したテンプレート例です。どちらでも好きなORMを使ってください。

https://github.com/j5ik2o/scala-ddd-base/blob/reboot/example/templates/template.ftl

テンプレートの書き方はこちら参照。カラム名をあらかじめプロパティ名としてテンプレートコンテキストに含めているので、簡単に書けるはずです。

build.sbt

  • flywayプロジェクト

https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L114-L136

このプロジェクトではchatwork/sbt-wix-embedded-mysqlとflyway/flyway-sbtを使って自動的にスキーマを作ります。flywayMigrate := (flywayMigrate dependsOn wixMySQLStart).value としているので、sbt flyway/flywayMigrateする前に組み込みMySQLが起動します。

  • exampleプロジェクト

https://github.com/j5ik2o/scala-ddd-base/blob/master/build.sbt#L138-L188

JDBCの接続先設定は、flywayプロジェクトと同じ設定を指定してください。

このプロジェクトでは、septeni-original/sbt-dao-generatorを使ってJDBCメタ情報とテンプレートをマージして、DAOクラスのソースコードを生成します。 今回は生成物をGitで管理したかったので、以下のようにして通常のソースコードと同じパスに出力していますが、(sourceManaged in Compile).valueを使ってtarget/src_managedに出力することも可能です。

outputDirectoryMapper in generator := {
  case s if s.endsWith("Spec") => (sourceDirectory in Test).value
  case s => new java.io.File((scalaSource in Compile).value, "/com/github/j5ik2o/dddbase/example/dao")
},
outputDirectoryMapper in generator := { className: String => (sourceManaged in Compile).value },

コンパイル時にソースコード生成するには以下のようにしてください。コンパイルと無関係に生成タスクを実行したい場合はsbt generator::generateAllとしてください。

// sourceGenerators in Compile時に出力
sourceGenerators in Compile += (generateAll in generator).value

コンパイルより前に出力したい場合は以下でも動作します。

// コンパイルより前に出力
compile in Compile := ((compile in Compile) dependsOn (generateAll in generator)).value

あとでexampleプロジェクトからflywayプロジェクトに依存することをお忘れ無く

val example = ...
dependsOn(..., flyway)

生成

コンパイル時に自動的に以下が行われます。

  1. 組み込みMySQLの起動
  2. flyway マイグレーション実行
  3. JDBCメタ情報とテンプレートのマージとソースファイル出力
  4. コンパイル

実際に生成されたソースコードはこちら。

https://github.com/j5ik2o/scala-ddd-base/blob/master/example/src/main/scala/com/github/j5ik2o/dddbase/example/dao/UserAccount.scala

package com.github.j5ik2o.dddbase.example.dao

package slick {
  import com.github.j5ik2o.dddbase.slick.SlickDaoSupport

  trait UserAccountComponent extends SlickDaoSupport {

    import profile.api._

    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 SoftDeletableRecord

    case class UserAccounts(tag: Tag)
        extends TableBase[UserAccountRecord](tag, "user_account")
        with SoftDeletableTableSupport[UserAccountRecord] {
      // def id = column[Long]("id", O.PrimaryKey)
      def status    = column[String]("status")
      def email     = column[String]("email")
      def password  = column[String]("password")
      def firstName = column[String]("first_name")
      def lastName  = column[String]("last_name")
      def createdAt = column[java.time.ZonedDateTime]("created_at")
      def updatedAt = column[Option[java.time.ZonedDateTime]]("updated_at")
      override def * =
        (id, status, email, password, firstName, lastName, createdAt, updatedAt) <> (UserAccountRecord.tupled, UserAccountRecord.unapply)
    }

    object UserAccountDao extends TableQuery(UserAccounts)

  }

}

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)

    }

  }

}

まとめ

このプロジェクト構成は、仕事でも結構がっつり使っていて気に入っています。スキーマ変更が起こっても、DAOは一瞬で自動生成できるので楽になると思います。興味あれば使ってみてください!


  1. 僕とセプテーニさんとコラボして作りました。

jupyter-scala の セットアップ方法と使い方

セットアップ

$ pyenv install 3.6.5
$ pyenv virtualenv 3.6.5 jupyter-notebook
$ cd jupyter-scala
$ pyenv local jupyter-notebook
$ pip install --upgrade pip
$ pip install jupyter-notebook
$ wget https://raw.githubusercontent.com/alexarchambault/jupyter-scala/master/jupyter-scala
$ sh ./jupyter-scala

wgetしたjupyter-scalaSCALA_VERSIONは以下のように2.11系なので、2.12を使う場合はコメントどおりに修正する。

SCALA_VERSION=2.11.11 # Set to 2.12.2 for Scala 2.12

起動

$ jupyter notebook

デフォルトブラウザ上で、http://localhost:8888/treeが開かれます。

使い方

NewScalaをクリックします。

jn-home.png

Hello Worldをやってみましょう。コードを入力したらShift+Enterで実行することができます。typoしてもセル内のコードを修正して実行し直すことができる

jn-helloworld.png

外部ライブラリへの依存関係を追加する

build.sbtlibraryDependenciesに書く情報を以下の形式にして入力・評価するだけでダウンロードされます。他に依存関係を除外する$execludeもあるようです。1

import $ivy.`org.typelevel::cats-core:1.1.0`

jn-import.png

NOTE

load.resolverに相当するAPIがよくわからなかった。ここにResolverを追加すればよさそうではあるが、まだ試していない。


  1. このイシュー によるとload.ivyclasspath.addなど古いAPIとのこと。

FluxとDDDの統合方法

おはこんばんにちは、かとじゅんです。 久しぶりにブログを書く…。最近、趣味でAngular2やらReactやらやっています。やっとWebpackになれました…。

さて、今回のお題は「FluxとDDDの統合方法」について。Angular2を先に触っていましたが、FluxといえばやはりReactだろうということで途中で浮気してReactで考えています。Angular2でもできるはずですが、今回はReactで統合方法*1について考えてみたいと思います。一つ断っておくと、FluxはDDDと統合することを想定していない設計パターンなんで云々とかはここでは考えていません。それはこのブログ記事を読む読まないに関わらずご自身で判断されてください。ソースコードについては、Githubへのリンクを一番下に書いてあるので興味がある人は参考にしてみてください。

*1:今回はEvent Sourcingではありません。State Sourcingです。機会があれば実装例を作ります

続きを読む