S-JIS[2011-03-05] 変更履歴

Scala JDBC insert

ScalaJDBCを使ってテーブルにinsertしてみる。


Statementでinsert

まずは何の変哲も無い単なるinsert文。

val sql = """insert into EMP
(EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO)
values(8001, 'name8001', 'job8001', 0, '2011-03-05', 10000, NULL, 20)"""

val st = conn.createStatement
st.execute(sql)
st.close
conn.commit

※日付型にデータを入れるときはOracleだとto_date関数を使うべきだが、ここでは手抜きして文字列を直接指定…


PreparedStatementでinsert

次いで、PreparedStatementにしてみる。
ただ単に値をセットしていくだけではつまらないので、項目名と値をペアにして定義するようにしてみる。
(項目名一覧と値一覧が分離しているのは分かりづらいし、順序を変えたい場合等に2箇所に手を入れることになるから、一元管理したい)

val vs = List(("EMPNO", 8002), ("ENAME", "name8002"), …)

そういえばペア(値が2個のタプル)を使うなら「->」が使えるので、そちらの方が分かりやすそう。
insertで使う目的だから、左側を値にして右側を項目名にする方が“らしく見える”かな?

val NULL:Any = null
val vs = List(8002->"EMPNO", "name8002"->"ENAME", "job8002"->"JOB",
  0->"MGR", "2011-03-05"->"HIREDATE", 10000->"SAL", NULL->"COMM", 20->"DEPTNO")

※「null->"COMM"」はエラーになるので、「(null, "COMM")」と書くか、Null型でないnullを定義(NULL:Anyとか)して「->」を使う。

val sql = "insert into EMP (" + vs.map(_._2).mkString(", ") + ") values (" + vs.map(_=>'?').mkString(", ") + ")"
文字列の結合(+演算子)を使う場合、REPL上では式の途中で改行できない
val sql = """insert into EMP (
 """ + vs.map(_._2).mkString(", ") + """
) values (
 """ + vs.map(_=>'?').mkString(", ") + """
)"""
複数行リテラルを使って文字列内で改行する例。
val sql = <s>insert into EMP (
 { vs.map(_._2).mkString(", ") }
) values (
 { vs.map(_=>'?').mkString(", ") }
)</s>.text
XMLリテラルを使って文字列内で改行する例。
val st = conn.prepareStatement(sql)
var n = 1
vs.foreach{ t =>
  val v = t._1
  st.setObject(n, v)
  n += 1
}
インデックスを計算する変数を外に用意する方法。
varはあんまり使いたくないけど…。
vs.zipWithIndex.foreach{ t =>
  val ((v,_), n) = t
  st.setObject(n+1, v)
}
zipWithIndexは、要素とインデックスのペアを作り出す。
vs.zipWithIndex.foreach{ t =>
  val v = t._1._1
  val n = t._2
  st.setObject(n+1, v)
}
vs.zipWithIndex.foreach(t => st.setObject(t._2 +1, t._1._1))
vs.zipWithIndex.foreach{
  case ((v,_), n) => st.setObject(n+1, v)
}
vs.foldLeft(1){ (n,t) =>
  st.setObject(n, t._1)
  n + 1
}
foldLeftでインデックスを渡せる(計算できる)けど、
こういう使い方は良くないだろうなぁ(苦笑)
きっと、foldLeftは副作用を起こす処理で使うものじゃない。
st.executeUpdate
st.close
conn.commit

ケースクラスを利用

上記のリストの持ち方では、複数データをインサートしたい場合には、毎回項目名をリストに入れなきゃいけなくて、いまいち。
値を入れるバリューオブジェクト的なクラスを作って、そこに入れたデータを扱えると便利そう。
Scalaでバリューオブジェクト的なクラスと言えば、ケースクラス

case class Emp(
  empno: Int,
  ename: String,
  job: String,
  mgr: Int,
  hiredate: Date,
  sal: Double,
  comm: Double,
  deptno: Int
)

def sdate(y:Int, m:Int, d:Int) = {
  val c = java.util.Calendar.getInstance
  c.clear
  c.set(y, m-1, d)
  new Date(c.getTimeInMillis)
}

val e1 = Emp(8003, "name8003", "job8003", 0, sdate(2011,3,5), 10000, 0, 20)

val cs = List("EMPNO", "ENAME", "JOB", "MGR", "HIREDATE", "SAL", "COMM", "DEPTNO")
val sql = "insert into EMP (" + cs.mkString(", ") + ") values (" + cs.map(_=>'?').mkString(", ") + ")"
val st = conn.prepareStatement(sql)

// 個々の値をセット
st.setInt(1, e1.empno)
st.setString(2, e1.ename)
st.setString(3, e1.job)
st.setInt(4, e1.mgr)
st.setDate(5, e1.hiredate)
st.setDouble(6, e1.sal)
st.setDouble(7, e1.comm)
st.setInt(8, e1.deptno)

st.executeUpdate
st.close
conn.commit

さて、出来るには出来たけど、いちいちPreparedStatementに値をセットしていく(コーディングをする)のが面倒。
そういえばケースクラスには値をイテレーターで取得する方法がある。

// 個々の値をセット
e1.productIterator.zipWithIndex.foreach{ case(v,n) => st.setObject(n+1, v) }

どうせならテーブルの項目名も取りたいところだが、ケースクラスのフィールド名一覧を取得するメソッドは無いんだよなぁ。
Enumerationではリフレクションを使って名前を取得しているので、真似てみよう。
Scalaではフィールドの値を取得するように見せかけて実際にはフィールド名と同名のメソッドが定義されているので、メソッド一覧を取得してやればいい。
ただ、toString()だのhashCode()だのといった関係ないメソッドまで取れてしまうので、選別する必要はある。
今回はテーブルの項目名が相手なので、全部大文字のメソッドがあったら項目名とみなすことにしよう。

case class Emp(
  EMPNO: Int,
  ENAME: String = null,
  JOB: String = null,
  MGR: java.lang.Integer = null,
  HIREDATE: Date = null,
  SAL: java.lang.Double = null,
  COMM: java.lang.Double = null,
  DEPTNO: java.lang.Integer = null
)
//EMPNOはNOT NULL項目だがその他はNULL可なので、デフォルト値をnullにしてみた。
//数値項目にもnullを入れたいので、ScalaのIntやDoubleでなく、Javaのラッパークラスにしてみた。ラッパーでも、暗黙変換があるので普通に数値を代入できる。

val e2 = Emp(8004, "name8004", "job8004", 0, sdate(2011,3,5), SAL=10000, DEPTNO=20)

// メソッド一覧取得
val ms = classOf[Emp].getMethods.filter(_.getName.forall(_.isUpper))

val sql = "insert into EMP (" + ms.map(_.getName).mkString(", ") + ") values (" + Array.fill(ms.size)('?').mkString(", ") + ")"
val st = conn.prepareStatement(sql)

// 個々の値をセット
ms.zipWithIndex.foreach{ case(m,n) => st.setObject(n+1, m.invoke(e2)) }

st.executeUpdate
st.close
conn.commit

あ、「_」を項目名に含めることも考えるなら、isUpperを呼ぶんじゃなくてtoUpperCaseで変換して比較した方がいいかも。

// メソッド一覧取得
val ms = classOf[Emp].getMethods.filter{m => val s = m.getName; s==s.toUpperCase}

あと、Empクラスの複数データを作りたい場合は 都度新しいインスタンスを作るのが常套手段だと思うけど、
一部分だけ変えたいなら、copyメソッドを使って新しいインスタンスを生成するか、
ケースクラス定義時に各パラメーターに「var」を付けて、後から変更できるようにすればいい。
(JDBCの実行方法的には、executeBatch()も視野に入れる)


JDBC実験へ戻る / Scala実験へ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま