SquerylでPostgreSQLのgeometry関連関数を使う
以前別ブログで、Squerylで最初からは用意されていない集計関数(median)を実装する、というエントリを書いた。今回はそれと似た内容で、PostgreSQLの位置情報の関数をSquerylで使えるようにしてみる。
Squeryl 0.9.5-6
距離を調べるSQL
今回使いたいのは、2点間の距離を調べる <-> という関数。例えば、以下のクエリーのように、ある位置(36.726637,139.526557)から3000m以内にある点のうち、距離が近い順に30件取得したいとする。これをSquerylを使って実現したい。
SELECT p.*, (p.latlng <-> POINT(36.726637,139.526557)) * 111000 as l FROM place p WHERE (p.latlng <-> POINT(36.726637,139.526557)) < 3000.0 / 111000.0 ORDER BY p.latlng <-> POINT(36.726637,139.526557) LIMIT 30
Squerylでの定義
先に結論から書いておく。以下のように定義した。もっといい方法があるかもしれない。特に、StringExpression を受け取る所がイマイチな気がする。
def distance(col1: StringExpression[String], col2: StringExpression[String]) = new FunctionNode[Double]("distance", None, Seq(col1, col2)) with NumericalExpression[Double] { override def doWrite(sw: StatementWriter) = { col1.write(sw) sw.write(" <-> ") col2.write(sw) } }
使い方
上で定義したdistance関数をどのように使うかについて、以下に述べる。
point型を含むテーブルに対応したクラスの定義
クラス定義は以下の通り。ポイントはPostgreSQLのpoint型のカラム(latlng)はString型のフィールドで値を受け取り、それを自作のPointクラス(後述)に渡して使用する。
class Place ( val id: Int = 0, @Column("latlng") val rawLatLng: String ) { def this() = { this(0, "") } def this(id: Int, latlng: Point) = { this(id, latlng.toString) } def latlng: Point = { Point(rawLatLng) } }
クエリーの組み立て
先ほど作った”distance”関数を使って、以下のように書ける。ポイントは、distance関数にはStringを渡すこと。
from(places)(p => select(p) where(distance(p.rawLatLng, Point(lat, lng).toString) < (3000.0 / 111000.0)) orderBy(distance(p.rawLatLng, Point(lat, lng).toString))
Pointクラス?
上のほうで出た、Pointクラスは以下のように定義した。
case class Point ( val lat: Double, val lng: Double ) { override def toString = { "(%f,%f)".format(lat, lng) } } object Point { def apply(s: String): Point = { parseStr(s) } private def parseStr(pointStr: String): Point = { // 省略 } }
少しSquerylのソースも見てみる
distanceメソッドは、SquerylのFunctionNodeクラスを使用しているが、FunctionNodeクラスのコンストラクタのシグニチャーは以下の通り。これと先ほどのdistance関数を見比べてみれば意味が分かりやすいかも。
class FunctionNode[A](val name: String, _mapper : Option[OutMapper[A]], val args: Iterable[ExpressionNode]) extends ExpressionNode {
また、distance関数はNumericalExpression traitをmix-inしている。それにより、結果がdouble型で返される。
まとめ
Squerylが直接サポートしていないRDBMSの関数(今回は<->)でも、自分で少しコードを書けばSquerylで使えるようになる。
この辺のドキュメントはあまりないので、ソースを見るのが一番早いかも。
メモ: このスライド(?)も少し参考した。
Squerylでgeometry関連型を使った例はありますか?