slickでtuple22問題を回避する
以前、社内用に書いたものから転載。 以前Playframework1.2.5で作った管理ツールをplay2.1.*でリプレイスするときにtuple22問題にあたったのでその回避策を。 Scalaの新しいバージョンはtupleの数も増えるらしいし、こんな問題は起きないようになるのかも。
Slickは非常に直感的で使いやすいORMだが、タプルベースの実装のため、22カラムまでしか扱うことができない。
回避策は主に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