slickでtuple22問題を回避する

以前、社内用に書いたものから転載。 以前Playframework1.2.5で作った管理ツールをplay2.1.*でリプレイスするときにtuple22問題にあたったのでその回避策を。 Scalaの新しいバージョンはtupleの数も増えるらしいし、こんな問題は起きないようになるのかも。

Slickは非常に直感的で使いやすいORMだが、タプルベースの実装のため、22カラムまでしか扱うことができない。

回避策は主に3つあって

  1. テーブルを分割する

  2. タプルをネストさせる

  3. 必要なカラムだけをクラス定義に含める

(引用 http://d.hatena.ne.jp/tototoshi/20121204/1354615421)

リプレイスということもあり、すでに23カラム以上あるテーブルが存在していたので、タプルをネストする方法にした。 泥臭い方法だけど、「22カラムまでか〜」という理由で採用を見送られていることもあるかもなので、いける方法はあるよとの参考になれば。


レポートテーブルの中には1時間ごとのクリック数がはいっている。

mysql> desc report;
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| id         | bigint(20)  | NO   | PRI | NULL    | auto_increment |
| article_id | bigint(20)  | YES  | MUL | NULL    |                |
| click00    | bigint(20)  | YES  |     | NULL    |                |
| click01    | bigint(20)  | YES  |     | NULL    |                |
| click02    | bigint(20)  | YES  |     | NULL    |                |
| click03    | bigint(20)  | YES  |     | NULL    |                |
| click04    | bigint(20)  | YES  |     | NULL    |                |
| click05    | bigint(20)  | YES  |     | NULL    |                |
| click06    | bigint(20)  | YES  |     | NULL    |                |
| click07    | bigint(20)  | YES  |     | NULL    |                |
| click08    | bigint(20)  | YES  |     | NULL    |                |
| click09    | bigint(20)  | YES  |     | NULL    |                |
| click10    | bigint(20)  | YES  |     | NULL    |                |
| click11    | bigint(20)  | YES  |     | NULL    |                |
| click12    | bigint(20)  | YES  |     | NULL    |                |
| click13    | bigint(20)  | YES  |     | NULL    |                |
| click14    | bigint(20)  | YES  |     | NULL    |                |
| click15    | bigint(20)  | YES  |     | NULL    |                |
| click16    | bigint(20)  | YES  |     | NULL    |                |
| click17    | bigint(20)  | YES  |     | NULL    |                |
| click18    | bigint(20)  | YES  |     | NULL    |                |
| click19    | bigint(20)  | YES  |     | NULL    |                |
| click20    | bigint(20)  | YES  |     | NULL    |                |
| click21    | bigint(20)  | YES  |     | NULL    |                |
| click22    | bigint(20)  | YES  |     | NULL    |                |
| click23    | bigint(20)  | YES  |     | NULL    |                |
| date       | date        | YES  |     | NULL    |                |
| referer    | varchar(64) | NO   |     | none    |                |
+------------+-------------+------+-----+---------+----------------+
28 rows in set (0.05 sec)

カラムを表現するにはtupleが足りないのでtupleをネストした状態でcase classを宣言する。

case class ReportClick(click00: Long,
                       click01: Long,
                       click02: Long,
                       click03: Long,
                       click04: Long,
                       click05: Long,
                       click06: Long,
                       click07: Long,
                       click08: Long,
                       click09: Long,
                       click10: Long,
                       click11: Long)

case class Report(id: Long,
                  articleId: Long,
                  clickAm: ReportClick,
                  clickPm: ReportClick,
                  date: java.sql.Date)

カラムは全部宣言する必要がある。

object Reports extends BaseModel.BaseTable[Long]("report") {
  import BaseModel.profile.simple._

  def id = column[Long]("id", O PrimaryKey, O AutoInc, O DBType "bigint(20)")
  def articleId = column[Long]("article_id", O DBType "bigint(20)")
  def click00 = column[Long]("click00")
  def click01 = column[Long]("click01")
  def click02 = column[Long]("click02")
  def click03 = column[Long]("click03")
  def click04 = column[Long]("click04")
  def click05 = column[Long]("click05")
  def click06 = column[Long]("click06")
  def click07 = column[Long]("click07")
  def click08 = column[Long]("click08")
  def click09 = column[Long]("click09")
  def click10 = column[Long]("click10")
  def click11 = column[Long]("click11")
  def click12 = column[Long]("click12")
  def click13 = column[Long]("click13")
  def click14 = column[Long]("click14")
  def click15 = column[Long]("click15")
  def click16 = column[Long]("click16")
  def click17 = column[Long]("click17")
  def click18 = column[Long]("click18")
  def click19 = column[Long]("click19")
  def click20 = column[Long]("click20")
  def click21 = column[Long]("click21")
  def click22 = column[Long]("click22")
  def click23 = column[Long]("click23")
  def date = column[java.sql.Date]("date", O DBType "date")
  ...
}

ケースクラスへのマッピングメソッドはオーバーライドする必要があるので適当に書く。 allメソッドがケースクラスであるタプルとその内包するタプルの要素のマッピング。

def * = id

def all = (
  id,
  articleId,
  (click00, click01, click02, click03, click04, click05,
   click06, click07, click08, click09, click10, click11),
  (click12, click13, click14, click15, click16, click17,
   click18, click19, click20, click21, click22, click23),
  date
  )

override def create_* =
  all.shaped.packedNode.collect {
    case Select(Ref(IntrinsicSymbol(in)), f: FieldSymbol) if in == this => f
  }.toSeq.distinct

できあがったつらいinsert

  def insert(report: Report)(implicit session: Session) = {
    // カウント数を取得する
    val inc = this.count() + 1
    val data = (inc.toLong, report.articleId, report.referer,
      (report.clickAm.click00, report.clickAm.click01, report.clickAm.click02,
       report.clickAm.click03, report.clickAm.click04, report.clickAm.click05,
       report.clickAm.click06, report.clickAm.click07, report.clickAm.click08,
       report.clickAm.click09, report.clickAm.click10, report.clickAm.click11),
      (report.clickPm.click00, report.clickPm.click01, report.clickPm.click02,
       report.clickPm.click03, report.clickPm.click04, report.clickPm.click05,
       report.clickPm.click06, report.clickPm.click07, report.clickPm.click08,
       report.clickPm.click09, report.clickPm.click10, report.clickPm.click11),
      report.date)
    Reports.all.shaped.insert(data)
    inc.toLong
  }

findはオーバーライドしたallを使って書く

def findAll()(implicit session: Session) = {
    val q1 = Reports.map(_.all)
    q1.list()
  }

クリック数の合計値を算出して合計値でソートしている処理の例

  def sortByClickSum(l: List[(Long, Long, String, (Long, Long, Long, Long, Long, Long, Long, Long, Long, Long, Long, Long)
    , (Long, Long, Long, Long, Long, Long, Long, Long, Long, Long, Long, Long), java.sql.Date)]) = {
    val res = l.map{ r =>
      val click = r._4.productIterator.toList ::: r._5.productIterator.toList
      val total = click.reduceLeftOption((z, n) => z.asInstanceOf[Long] + n.asInstanceOf[Long]) match {
        case Some(x: Long) => x
        case None => 0
      }

      (r._2, total)
    }
    res.sortBy(_._2).reverse
  }

結論としてはおすすめしない。Slickのすっきり書ける感もない。 どうしてもslickを使っていてtuple足りないといったときの苦肉の策。 前もって22カラムを超えることが判明しているならば、別の方法を考えよう。

抜粋的なソースはこちら。

https://github.com/wshino/avoid-tuple22-problem-for-play-slick

Published: October 23 2013

  • category:
  • tags:
blog comments powered by Disqus