2018-12-08

Slick でちょっと複雑なテーブルを定義して case class にマッピングする方法

こちらは Shanon Advent Calendar 2018 の7日目の記事です
tado56です


最近,Scala で PostgreSQL を扱うことがあり,Slick を使いました.
Slick でテーブルを定義して case class にマッピングする方法を,簡単なものから少し複雑なものまで順番に紹介します.

一番シンプルな定義
一番単純なマッピングの方法です.
case class Example( id: Int, name: String )
class ExampleTable( tag: Tag, schema_name: String, table_name: String ) 
   extends Table[ Example ]( tag, Some(schema_name), table_name) {
 def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
 def name = column[String]("name")

 def * = (id, name) <> (Example.tupled, Example.unapply)
}

自動採番される id と name カラムを持ったテーブルを定義できます.
このままだと両方のカラムは NOT NULL制約を持っていますが,
column[Option[String]] のように name を定義すると, NOT NULL 制約を外せます.

カラムとクラスを1対1でマッピングする
case class のフィールドの型にクラスを持たせることができます.
その時,単一のカラムであれば,MappedTo を使うことで通常のカラム型と1対1で紐づけることができます.
case class ExampleId( value: Int ) extends MappedTo[Int]
case class Example( id: ExampleId, name: String )

class ExampleTable( tag: Tag, schema_name: String, table_name: String ) 
   extends Table[ Example ]( tag, Some( schema_name), table_name) {
  def id = column[ExampleId]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")

  def * = (id, nanika) <> (Example.tupled, Example.unapply)
}

複数のカラムを case class にマッピングする
上の例のように,単一ではなく複数のフィールドを持っていることの方が多いと思います.
その時には, * と同じように <> を使って定義します.
case class ExampleId( value: Int ) extends MappedTo[Int]
case class ExampleName( name1: String, name2: String )
case class Example( id: ExampleId, name: ExampleName )                                                               
class ExampleTable( tag: Tag, schema_name: String, table_name: String )
   extends Table[ Example ]( tag, Some( schema_name), table_name) {
  def id = column[ExampleId]("id", O.PrimaryKey, O.AutoInc)
  
  def name1 = column[String]("name1")
  def name2 = column[String]("name2")
  def name = (name1, name2) <> (ExampleName.tupled, ExampleName.unapply)

  def * = (id, name) <> (Example.tupled, Example.unapply)
}
ここまで作れれば,class のフィールドにclassの深さがいくら深くなっても基本的にはどのような定義が可能です.

完全に理解した!

ですが・・・
このようなクラスが存在することがあるかもしれません
case class Example( id: ExampleId, name: ExampleName, Age: Option[Age])
この時 Age は Option なので値を持たないときがあります.
このような時の実装でまず思いつくのが, Option だから NOT NULL 制約を外す必要があるんや!余裕やん!その時の書き方は・・・
case class ExampleId( value: Int ) extends MappedTo[Int]
case class ExampleName( name1: String, name2: String )
case class Age( age: Int, birthday: String )
case class Example( id: ExampleId, name: ExampleName, age: Option[Age])

class ExampleTable( tag: Tag, schema_name: String, table_name: String )
   extends Table[ Example ]( tag, Some( schema_name), table_name) {
  def id = column[ExampleId]("id", O.PrimaryKey, O.AutoInc)
  
  def name1 = column[String]("name1")
  def name2 = column[String]("name2")
  def name = (name1, name2) <> (ExampleName.tupled, ExampleName.unapply)

  def age_num = column[Option[Int]]("age")
  def birthday = column[Option[String]]("birthday")
  def age = (age_num, birthday) <> (Age.tupled, Age.unapply)

  def * = (id, name, age) <> (Example.tupled, Example.unapply)
}
このように書いてしまいがちです.(自分がそう書いたので他の人もそうだと決めつけます)
ですがこれではコンパイルが通りません!

以下のように tupled と unapply をカスタムしてあげる必要があります.
object Age {
  def fromOptions( age: Option[Int], birthday: Option[String] ): Option[ Age ] = 
    (age, birthday)match {
      case ( Some(a), Some(b) ) =>  
        Some( Age( a, b ) ) 
      case _ => None
    }

  def customTupled = (fromOptions _).tupled
  def customUnapply( age: Option[ Age ] ): Option[ (Option[Int], Option[String]) ] =                               
    age match {
      case None => (Some(None, None))
      case Some( x ) => age.map { a => (Some(a.age), Some(a.birthday))
    }   
  }
}

ここまでしてあげてようやく無事テーブルの定義を行うことができます.
case class ExampleId( value: Int ) extends MappedTo[Int]
case class ExampleName( name1: String, name2: String )
case class Example( id: ExampleId, name: ExampleName, age: Option[Age])

case class Age( age: Int, birthday: String )
object Age {
  def fromOptions( age: Option[Int], birthday: Option[String] ): Option[ Age ] = 
    (age, birthday)match {
      case ( Some(a), Some(b) ) =>  
        Some( Age( a, b ) ) 
      case _ => None
    }
  def customTupled = (fromOptions _).tupled
  def customUnapply( age: Option[ Age ] ): Option[ (Option[Int], Option[String]) ] =                               
    age match {
      case None => (Some(None, None))
      case Some( x ) => age.map { a => (Some(a.age), Some(a.birthday))
    }   
  }
}

class ExampleTable( tag: Tag, schema_name: String, table_name: String )
   extends Table[ Example ]( tag, Some( schema_name), table_name) {
  def id = column[ExampleId]("id", O.PrimaryKey, O.AutoInc)
  
  def name1 = column[String]("name1")
  def name2 = column[String]("name2")
  def name = (name1, name2) <> (ExampleName.tupled, ExampleName.unapply)

  def age_num = column[Option[Int]]("age")
  def birthday = column[Option[String]]("birthday")
  def age = (age_num, birthday) <> (Age.customTupled, Age.customUnapply)

  def * = (id, name, age) <> (Example.tupled, Example.unapply)
}

ここら辺の定義ができればほとんどの構造の定義はできるのではないかと思います.
以上です.

0 件のコメント:

コメントを投稿