ixias.model　IxiaS

# 参考

https://medium.com/nextbeat-engineering/%E8%87%AA%E7%A4%BEoss-ixias-%E3%81%AE%E7%B4%B9%E4%BB%8B-ixais-model%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%82%B3%E3%83%BC%E3%83%89-d6e0e5d8e8aa

# IxiaS とは

IxiaS とはネクストビートの衣笠氏が開発した OSS で、同社のプロダクト共通で用いられている Scala ライブラリ。

https://github.com/ixias-net/ixias

https://medium.com/nextbeat-engineering/%E9%96%8B%E7%99%BA%E8%80%85%E3%81%AFcto%E8%A1%A3%E7%AC%A0-%E3%83%97%E3%83%AD%E3%83%80%E3%82%AF%E3%83%88%E5%85%B1%E9%80%9A%E3%81%AEscala%E3%81%AE%E3%82%B3%E3%82%A2%E6%8A%80%E8%A1%93-ixias-%E3%82%92oss%E5%8C%96-3eb5c4ed66bf

## プロダクトの標準化

IxiaS によってプロダクトの作り方を標準化することができる。
IxiaS はフレームワークレイヤーの Play Framework 、 DB レイヤーの slick 、インフラ連携の AWS 等を利用しており、これらを利用する際のロジックを標準化している。
そのため、プロダクト間の人材ローテーションがある際も、プロダクトのコード理解が早く進みやすくなる。

## DDD を意識したビジネスモデルの枠組み

IxiaS は DDD を意識したビジネスモデルの枠組みを提供している。
DDD におけるエンティティや値オブジェクトといった概念が登場し、 [ixias.model.Entitiy](https://github.com/ixias-net/ixias/blob/develop/framework/ixias-core/src/main/scala/ixias/model/Entity.scala) (以下、 *Entity* ) クラスや型安全な識別子 ( *Id* ) 、列挙型 ( *Enum* ) などの値を実装している。

IxiaS のパッケージ群は *Entity* クラスを中心として実装されており、 *Entity* クラスを取り巻く処理に関するパッケージ構成（アーキテクチャ）がプロダクトを超えて共通化されやすくなっている。

# IxiaS インストール

`build.sbt` に以下を追加することで、 IxiaS を使用できる。

```scala:build.sbt
resolvers ++= Seq(
  "IxiaS Releases" at "http://maven.ixias.net.s3-ap-northeast-1.amazonaws.com/releases"
)

libraryDependencies ++= Seq(
  "net.ixias" %% "ixias"      % "1.1.11",
  "net.ixias" %% "ixias-aws"  % "1.1.11",
  "net.ixias" %% "ixias-play" % "1.1.11",
  "mysql"          % "mysql-connector-java" % "5.1.+",
  "ch.qos.logback" % "logback-classic"      % "1.1.+"
)
```

※注意点として、 Scala のバージョンは 2.12.x 以下にする必要がある。
これは、依存ライブラリのバージョン対応のため。

# ixias.model パッケージ

今回、 ixias-core ライブラリ内に実装されている [ixias.model パッケージ](https://github.com/ixias-net/ixias/tree/develop/framework/ixias-core/src/main/scala/ixias/model)　に関して、サンプルコードを交えて紹介していく。
ixias.model パッケージは DDD におけるエンティティに対応する枠組みを実装しており、 IxiaS の核となる存在。

ixias.model パッケージは、 [ixias.persistence パッケージ](https://github.com/ixias-net/ixias/tree/develop/framework/ixias-core/src/main/scala/ixias/persistence) と組み合わせることで初めて Web アプリケーションに組み込むことができる。
しかし、 ixias.persistence パッケージも合わせて紹介を行うと記事のボリュームが大きくなるので、今回は ixias.persistence パッケージの紹介は行わない。

さて、 ixias.model パッケージは以下のような実装によって使用する。

```scala:app/lib/model/User.scala
/**
  * This is a sample of Todo Application.
  * 
  */

package lib.model

import ixias.model._
import ixias.util.EnumStatus

import java.time.LocalDateTime

// ユーザーを表すモデル
//~~~~~~~~~~~~~~~~~~~~
import User._
case class User(
  id:        Option[Id],
  name:      String,
  age:       Short,
  state:     Status,
  updatedAt: LocalDateTime = NOW,
  createdAt: LocalDateTime = NOW
) extends EntityModel[Id]

// コンパニオンオブジェクト
//~~~~~~~~~~~~~~~~~~~~~~~~
object User {

  val  Id = the[Identity[Id]]
  type Id = Long @@ User
  type WithNoId = Entity.WithNoId [Id, User]
  type EmbeddedId = Entity.EmbeddedId[Id, User]

  // ステータス定義
  //~~~~~~~~~~~~~~~~~
  sealed abstract class Status(val code: Short, val name: String) extends EnumStatus
  object Status extends EnumStatus.Of[Status] {
    case object IS_INACTIVE extends Status(code = 0,   name = "無効")
    case object IS_ACTIVE   extends Status(code = 100, name = "有効")
  }

  // INSERT時のIDがAutoincrementのため,IDなしであることを示すオブジェクトに変換
  def apply(name: String, age: Short, state: Status): WithNoId = {
    new Entity.WithNoId(
      new User(
        id    = None,
        name  = name,
        age   = age,
        state = state
      )
    )
  }
}
```

## case class

case class には、 [ixias.model.EntityModel](https://github.com/ixias-net/ixias/blob/develop/framework/ixias-core/src/main/scala/ixias/model/EntityModel.scala) (以下、 *EntityModel* ) トレイトを継承する。
以降、 *EntityModel* トレイトを継承した case class のことを、簡単のため *EntityModel* クラスと呼ぶ。

継承する *EntityModel* トレイトは、型パラメータを 1 つ受け取る。
ここで受け取る型は、コンパニオンオブジェクト `object User` 内で定義している *Id* 。
*Id* については、後で説明する。

### 必須な値

*EntityModel* クラスには、 少なくとも　*id* 、 *updatedAt* 、 *createdAt* の 3 つの値を定義する。
これらは、 [*EntityModel* トレイト](https://github.com/ixias-net/ixias/blob/d8a26698653fc71ab7a77775a37b1298a9f9f068/framework/ixias-core/src/main/scala/ixias/model/EntityModel.scala#L16) に抽象な値として定義されており、命名と型を定められた通りに定義する必要がある。

*id* の値は、 *Option[Id]* 型で定義されている。
Option 型にすることで、 *Id* の有無を判別できる。

```scala:ixias-core/src/main/scala/ixias/model/EntityModel.scala(24)
val id: Option[Id]
```

*updatedAt* と *createdAt* は型として *java.time.LocalDateTime* を持つ。

```scala:model/EntityModel.scala(26~30)
  /** The date and time when this entity was last updated. */
  val updatedAt: LocalDateTime

  /** The date and time when this entity was added to the system. */
  val createdAt: LocalDateTime
```

この 2 つの値には、 [ixias.mode.package.scala](ixias.model.package.scala) (以下、 *package.scala* ) 内に定義されている *NOW* メソッドをデフォルト値として与えている。

```scala:app/lib/model/User.scala
  updatedAt: LocalDateTime = NOW,
  createdAt: LocalDateTime = NOW
```

なお、 *NOW* メソッドの定義は以下のとおり。

```scala:model/EntityModel.scala(29)
  def NOW = java.time.LocalDateTime.now()
```

## コンパニオンオブジェクト

次に、コンパニオンオブジェクトを見ていく。

### *Id* の定義

コンパニオンオブジェクト内には、 *Id* の型と値を定義する必要がある。
*Id* は、DDD における同一性を持つ識別子の役割を果たす。

*Id* の型は、 [*shapeless.tag.@@*](https://github.com/milessabin/shapeless/blob/7ede0867c6d5c95d2618702f140e24218bdaecaf/core/src/main/scala/shapeless/typeoperators.scala#L25) を用いて定義する。

```scala:app/lib/model/User.scala
  type Id = Long @@ User
```

@@ と *shapeless.tag.@@* の変換は、 *package.scala* で行われている。

```scala:model/package.scala(17)
  type @@[+T, U] = shapeless.tag.@@[T, U]
```

こちらは Tagged Type と呼ばれる手法で、 Long 型の値を扱う *Id* という異なる名前の型を持たせている。
*Id* は Long型または String 型を採用することが多い。

*Id* の値は [*shapless.the*](https://github.com/milessabin/shapeless/blob/7ede0867c6d5c95d2618702f140e24218bdaecaf/core/src/main/scala/shapeless/typeoperators.scala#L109) と [ixias.model.identity](https://github.com/ixias-net/ixias/blob/develop/framework/ixias-core/src/main/scala/ixias/model/Identity.scala) (以下、 *Identity* ) オブジェクトを用いて定義する。

```scala:app/lib/model/User.scala
  val  Id = the[Identity[Id]]
```

*the* と *shapeless.the* の変換は @@ と同様、 *pacakge.scala* で行われている。

```scala:model/package.scala(26)
  val the = shapeless.the
```

*Identity* は、 *shapeless.Unwrapped* で実装されており、 *Id* の生成や型に関する暗黙的な処理等を行ってくれる。

### コンストラクタの定義

最後に、コンパニオンオブジェクト内に以下のような *build* メソッドを定義し、 *Entity* クラスを生成するメソッドを用意する。
*apply* メソッドと使い分けられるよう、今回は *build* という命名にしているが、こちらは任意。

```scala
def build(name: String): User#WithNoId =
  new User(
    id = None,
    name = name,
  ).toWithNoId
```

返り値の型には *User#WithNoId* 型が定義されており、 *#* を用いることで *EntityModel* トレイト内の *WithNoId* 型にアクセスしている。

*new User(...)* で作成した *EntityModel* クラスから *EntityModel* トレイト内に定義されている *toWithNoId* メソッドを呼び出すことで、 *User#WithNoId* 型を返すことができる。

#### WithNoId と EmbeddedId の型の定義

*EntityModel* トレイト内には、先ほど *User#WithNoId* として登場した *WithNoId* 型と対になる関係を持つ *EmbeddedId* 型が定義されている。
2 つの型は以下のように実装されている。

```scala:model/EntityModel.scala(20~21)
  type WithNoId   = Entity.WithNoId  [Id, this.type]
  type EmbeddedId = Entity.EmbeddedId[Id, this.type]
```

これらは文脈を持つ型のような役割を持ち、 *WithNoId* は id を持たない *Entity* 、 EmbeddedId は id を持つ *Entity* という意味を持つ型。

ここで呼び出している *Entity.WithNoId* と *Entity.EmbeddedId* の型は、以下のように実装している。

```scala:model/Entity.scala(46,58)
  type   WithNoId[K <: @@[_, _], M <: EntityModel[K]] = Entity[K, M, IdStatus.Empty]

  type   EmbeddedId[K <: @@[_, _], M <: EntityModel[K]] = Entity[K, M, IdStatus.Exists]
```

いずれも、 1 つ目の型パラメータ *K* にはコンパニオンオブジェクト内で定義した *Id* を、 2 つ目の型パラメータ *M* には *EntityModel* クラスを受け取る。

呼び出し元である *EntityModel* トレイト内では、コンパニオンオブジェクト内で定義した *Id* と *EntityModel* トレイトを継承している *this.type* を与えることで、型パラメータの制約を満たしている。

そして、 *Entity.WithNoId* と *Entity.EmbeddedId* では、受け取った型である *K* と *M* を用いて、 *Entity* クラスを呼び出している。

ここで注目すべきは、 *Entity* クラスが受け取る 3 つ目の型パラメータ。
*WithNoId* と *EmbeddedId* の型の定義において、 3 つ目の型パラメータに与えている *IdStatus* オブジェクトは以下のように実装している。

```scala:model/Entity.scala(14~18)
trait  IdStatus
object IdStatus {
  trait Empty  extends IdStatus
  trait Exists extends IdStatus
}
```

この *IdStatus* オブジェクト内に定義している *Empty* と *Exists* を用いて、 *WithNoId* と *EmbeddedId* の判別を行う。

#### WithNoId と EmbeddedId の値の生成

次に、 *WithNoId* と *EmbeddedId* の型を持つ値を生成する *toWithNoId* メソッドと *toEmbeddedId* メソッドについて説明する。
2 つのメソッドは以下のように実装する。

```scala:model/EntityModel.scala(33~34)
  def toWithNoId:   WithNoId   = Entity.WithNoId(this)
  def toEmbeddedId: EmbeddedId = Entity.EmbeddedId(this)
```

それぞれ、自身のインスタンスである *this* を引数として *Entity* クラス内のオブジェクトを呼び出している。

`toWithNoId` メソッドを例に挙げると、 `toWithNoId` メソッドが呼び出している *Entity.WithNoId* オブジェクトの実装は以下のようになっている。

```scala:model/Entity.scala(47~54)
  object WithNoId {
    /** Create a entity object with no id. */
    def apply[K <: @@[_, _], M <: EntityModel[K]](data: M): WithNoId[K, M] =
      data.id match {
        case None    =>       new Entity(data)
        case Some(_) => throw new IllegalArgumentException("The entity's id is already set.")
      }
  }
```

与えられた *EntityModel* クラスの *id* を match 式で判別することで、エラーハンドリングされる実装となっている。
*new User(...)* 内の *id* の値に Option 型の *None* を与えているのはそのため。

`toWithNoId` メソッドに対して `toEmbeddedId` メソッドは、 `EmbeddedId` 型の値を生成する。
しかし、 IxiaS では *Entity.EmbeddedId()* を用いるユースケースはあまりない。

理由としては、 ixias.persistence パッケージを組み合わせて永続ストレージとの連携を行う際、永続ストレージへのデータ取得リクエストによる返り値の型が *EmbeddedId* にマッピングされる仕様になっている体。

### *WithNoId* と *EmbeddedId* を用いた処理の流れ

では、 *WithNoId* と *EmbeddedId* が実際にどのように用いられるかについて、簡単に紹介する。

永続ストレージとの連携を行う Repository オブジェクトへと継承をすることになる [ixias.persistence.dbio.EntityIOAction](https://github.com/ixias-net/ixias/blob/develop/framework/ixias-core/src/main/scala/ixias/persistence/dbio/EntityIOAction.scala) トレイトには、 CRUD 処理を行うための以下のような抽象メソッドが用意されている。

```scala:persistence/dbio/EntityIOAction.scala(54,73,79,85)
  def get(id: Id): Future[Option[EntityEmbeddedId]]
  def add(entity: EntityWithNoId): Future[Id]
  def update(entity: EntityEmbeddedId): Future[Option[EntityEmbeddedId]]
  def remove(id: Id): Future[Option[EntityEmbeddedId]]
```

例として、これらのメソッドを用いた User ケースクラスのライフサイクルは以下のような流れになる。

1. User のコンストラクタによって *WithNoId* 型の *Entity* インスタンスを生成
1. 生成した *WithNoId* 型のインスタンスを `add` メソッドに渡し、永続ストレージにレコードを追加
1. `get`メソッドの引数に *Id* を渡し、該当するレコードを *EmbeddedId* 型の *Entity* インスタンスとして取得
1. 取得した *EmbeddedId* 型のインスタンスに更新処理を加え、 `update` メソッdの引数に渡すことでレコードを更新
1. `delete` メソッドの引数に *Id* を渡し、該当するレコードを削除

このように CRUD 処理等のメソッドの命名や引数と返り値の型を統一することで、プロダクトの標準化にも繋がっている。

# console で実行

では、これまで紹介した内容の順に追っていく形で、 console を用いて実行していく。

## 生成

プロジェクト内の lib.model パッケージにサンプルコードと同様の User が定義されている想定で進めていく。
以下のコードで *lib.model.User* を import する。

```scala:REPL
scala> import lib.model.Userimport lib.model.User
```

`build` メソッドで *WithNoId* 型の *Entity* クラスインスタンスを生成する。
通常の命名は *user* で十分だが、今回は型を明示的にするため、 *userWithNoId* という命名にする。
(下記のコードでは、メソッド名を `build` ではなく `apply` としている)

```scala:REPL
scala> val userWithNoId: User#WithNoId = User.apply(name = "hogehoge", age = 20, state =  lib.model.User.Status(0))
userWithNoId: ixias.model.Entity[Long with shapeless.tag.Tagged[lib.model.User],lib.model.User,ixias.model.IdStatus.Empty] = Entity(User(None,hogehoge,20,IS_INACTIVE,2021-08-04T16:53:00.681,2021-08-04T16:53:00.681))
```

*WithNoId* 型であるため、 *id* の値が None になっていることがわかる。

また、 *Entity* クラスの識別子となる *Id* は以下のように生成する。

```scala:REPL
scala> val userId = User.Id(1)
userId: lib.model.User.Id = 1
```

*EmbeddedId* 型のインスタンスも説明用として生成する。
先ほどの *userWithNoId* を用いて永続ストレージにレコードを追加した後、永続ストレージからレコードを取得したという想定だ。

```scala:REPL
scala> val userEmbeddedId: User#EmbeddedId = new User(id = Some(userId), name = "hogehoge", age = 20, state =  lib.model.User.Status(0)).toEmbeddedId
userEmbeddedId: ixias.model.Entity[Long with shapeless.tag.Tagged[lib.model.User],lib.model.User,ixias.model.IdStatus.Exists] = Entity(User(Some(1),hogehoge,20,IS_INACTIVE,2021-08-04T21:00:05.959,2021-08-04T21:00:05.959))
```

## 取得

*Entity* クラスインスタンスは *user.v.name* のように *v (value)* に続けて入力することで、 *EntityModel* クラス内の値を取得できる。
*EmbeddedId* 型の場合も同様だ。

```scala:REPL
scala> userWithNoId.v.name
res0: String = hogehoge
```

しかし、 *id* の値のみ特別に *v* を用いずに取得できる。
理由としては、 *Entity* クラス内に *id* という名前のメソッドが定義されているためだ。
*id* メソッドは以下のように実装している。

```scala:model/Entity.scala(29~30)
  /** get id value when id is exists */
  def id(implicit ev: S =:= IdStatus.Exists): K = v.id.get
```

=:= を用いた型制約を暗黙的に与えることにより、型パラメータ *S* に *IdStatus.Exists* が与えられている *EmbeddedId* でないと `id` メソッドが提供されない仕様になっている。

そのため、型が *WithNoId* である *user* は `id` メソッドを呼ぶとエラーが起きる。

```scala:REPL
scala> userWithNoId.id
<console>:14: error: Cannot prove that ixias.model.IdStatus.Empty =:= ixias.model.IdStatus.Exists.
       userWithNoId.id
                    ^
```

*EmbeddedId* 型の場合はエラーが起きずに *id* を取得できる。

```scala:REPL
scala> userEmbeddedId.id
res2: Long with shapeless.tag.Tagged[lib.model.User] = 1
```

*EmbeddedId* は id を持っていることを保証する型であるため、 Option 型で返す必要がない。

この制約によりコンパイル時にエラーとなり、障害を減らすことができる。

## 更新

*Entity* には `map` メソッドが用意されており、 Function を適用した新たな *Entity* を返すことができる。
`map` メソッドは以下のように実装している。

```scala:model/Entity.scala
  /** Builds a new `Entity` by applying a function to values. */
  @inline def map[M2 <: EntityModel[K]](f: M => M2): Entity[K, M2, S] = new Entity(f(v))
```

*Entity* の更新を行いたい場合は、 `map` メソッドを用いて以下のように実装する。

```scala:REPL
scala> val updatedUser: User#EmbeddedId = userEmbeddedId.map(_.copy(name = "fugafuga")) 
updatedUser: ixias.model.Entity[Long with shapeless.tag.Tagged[lib.model.User],lib.model.User,ixias.model.IdStatus.Exists] = Entity(User(Some(1),fugafuga,20,IS_INACTIVE,2021-08-04T21:00:05.959,2021-08-04T21:00:05.959))
```