来世から頑張る!!

技術ブログを目指して

ScalaでSQLが書きたいんだ!!

DSLとかOR Mapperとかじゃなくて、SQLが直接書きたいんだ!!

嘘です。タイトル詐欺です。

最近neo4jというグラフ指向データベースがお気に入りでして、 アクセス用のクエリとしてCypherという言語を使います。

基本はHTTP+JSONでRESTにアクセスできるのですが、Java用にneo4j-jdbcというライブラリがあり、これを使ってアクセスしようと思うわけです。

そうすると、当然SQLではないのでDSLSQL自動生成とかは使えず、直接SQL(本当はCypher)を書ける機能が望まれるわけです。

neo4jのダウンロード

neo4jは全部Javaで出来ているらしいので、あなたとJavaで検索してJavaをインストールしてください。

その後、neo4j公式サイトから無料版をダウンロードしてきます。

適当な場所に展開して、bin/neo4j statusみたいな感じで実行するとlocalhost:7474でhttpを待ち受けてくれるようになります。

まずはブラウザでhttp://localhost:7474に繋いでパスワード変更をしましょう。

初期状態のユーザーはneo4j、パスワードも同じくneo4jですが、1度入力するとパスワード変更を要求されます。

ログインするとダッシュボードのような画面が表示され、 この画面からサンプルDBみたいなのの作り方やヘルプなどが見られるので始めは適当に触ってみるのがよいと思います。

仮データの登録

neo4jの画面の上部からCypherが入力できるので、データを登録してみましょう。

CREATE
  (p :Person{name: "kazzna", password: "some-password"}),
  (b :Blog{name: "kazzna's blog", url: "http://kazzna.hatenablog.com/"}),
  (p)-[:WRITES]->(b)
RETURN p, b

p, bが変数ですね。
細かい説明はしませんが、:Person型のノードと:Blog型のノードを1個ずつ作って、作った物を取得しています。

ついでにせっかくですのですてにゃんあたりを追加しておきましょう。

CREATE
  (s :Person{name: "stefafan"}),
  (sb :Blog{name: "すてにゃんのガチ勢日記", url: "http://stefafafan.hatenablog.com/"}),
  (s)-[:WRITES]->(sb)
WITH s, sb
MATCH (k :Person{name: "kazzna"}), (kb :Blog{name: "kazzna's blog"})
CREATE (s)-[:READS]->(kb), (k)-[:READS]->(sb)
RETURN s, k, sb, kb

kazznaはstefafafanのブログを読んで、stefafafanはkazznaのブログを読む、と。

:Personpasswordが無いのが特徴ですね。

Scalaから取得しよう!!

sbtは使える前提で話を進めますので、sbtって何?って人はtypesafe activatorでググってください。 で、以下のコマンド中のsbtactivatorに置き換えれば多分大丈夫です。

まずbuild.sbtファイルの設定。

name := "test"
version := "0.0.1"
lazy val root = (project in file("."))
scalaVersion := "2.11.7"

resolvers ++= Seq(
  "neo4j-public" at "http://m2.neo4j.org/content/groups/public",
  "jitpack" at "https://jitpack.io"
)

libraryDependencies ++= Seq(
  "org.scalikejdbc" %% "scalikejdbc" % "2.3.0",
  "com.github.kazzna" % "neo4j-jdbc" % "2.2-SNAPSHOT_1",
  "ch.qos.logback" % "logback-classic" % "1.1.3"
)

大体こんな感じ。

resolversを2つ追加しないといけないのは理由があって、 公式のneo4j-jdbcを使うだけなら1番上だけでいいのですが、 現在公式の最新版ではScalikeJDBCなどが自動で生成してくれる?を使ったPreparedStatementに対応していないのです。

なのでとりあえず?の自動で対応した形式に置き換える対応を行った自作版を使います。
JitPackという不思議な力を借りて、githubから自動で依存関係ライブラリーを作り出してもらいます。
(mavenのローカルリリースは使い方が分からなくて出来ませんでした。)

公式にはPull Requestがマージされたので、次回のバージョン(2.2系?)のリリースから使えるようになるはずです。

後は普通のSQLの時と同様ですね。sbt consoleして

scala> import scalikejdbc._
import scalikejdbc._

scala> case class Blog(name: String, url: String)
defined class Blog

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class Person(
  name: String,
  password: Option[String],
  blog: Option[Blog]
)

object Person extends SQLSyntaxSupport[Person] {
  def apply(rs: WrappedResultSet): Person = Person(
      rs.string("person.name"),
      rs.stringOpt("person.password"),
      rs.stringOpt("blog.name").flatMap { n =>
        rs.stringOpt("blog.url").map(u => Blog(n, u))
      })
}

// Exiting paste mode, now interpreting.

defined class Person
defined object Person

scala> Class.forName("org.neo4j.jdbc.Driver")
res0: Class[_] = class org.neo4j.jdbc.Driver

scala> ConnectionPool.singleton("jdbc:neo4j://localhost:7474/", "neo4j", "password")
14:09:20.332 [run-main-0] DEBUG scalikejdbc.ConnectionPool$ - Registered connection pool : ConnectionPool(url:jdbc:neo4j://localhost:7474/, user:neo4j) using factory : <default>
14:09:20.338 [run-main-0] DEBUG scalikejdbc.ConnectionPool$ - Registered singleton connection pool : ConnectionPool(url:jdbc:neo4j://localhost:7474/, user:neo4j)

scala> implicit val session = AutoSession
session: scalikejdbc.AutoSession.type = AutoSession

scala> val name = "kazzna"
name: String = kazzna

scala> val k = sql"MATCH (person :Person{name: ${name}}) WITH person OPTIONAL MATCH (person)-[:WRITES]->(blog) RETURN person.name, person.password, blog.name, blog.url".map(rs => Person(rs)).single.apply()
14:14:33.949 [run-main-0] DEBUG s.StatementExecutor$$anon$1 - SQL execution completed

// ScalikeJDBCの長いトレース

k: Option[Person] = Some(Person(kazzna,Some(some-password),Some(Blog(kazzna's blog,http://kazzna.hatenablog.com/))))

とりあえず普通に使えそうですね。

気付いたことと言えば、Cypherは更新+取得という文が書けるのですが、 それらはJDBCではupdateとしてしか発行できない為に戻り値は取れないようです。
(queryで発行するとreadonlyだよって怒られます。)

もう少し使ってみて、問題点が出ればまたプルリク投げようかなあと言う感じですね。

みなさんもぜひグラフDB使いましょう。

あ、今回は取り上げてないけれど、実際にはちゃんとインデックス貼りましょうね?
インデックスはほぼRDBと同じ感覚で張れば問題ないので。