geotrellis使用(三十)使用geotrellis读取PostGIS空间数据

前言

最近事情很多,各种你想不到的事情——such as singing and dancing——再加上最近又研究docker上瘾,所以geotrellis看上去似乎没有关注,其实我一直在脑中思考着geotrellis。之前看geotrellis源码看到有关geotrellis.slick的相关部分,仅大概浏览了一番,知道是用于读取PostGIS数据库的,未做深入研究,又恰巧前几日有老外在gitter上问了如何读取PostGIS数据库,我当时回答他可以用传统的JDBC方式或者使用geotrellis.slick。JDBC方式我是亲自测试过的,在geotrellis使用(十一)实现空间数据库栅格化以及根据属性字段进行赋值一文中,我详细讲述了如何从PostGIS中读取空间数据并进行栅格化操作;然而我也有极度强迫症,一个事物不知道还着罢了,一旦让我知道我是一定要拿来试试的,尤其在新技术方面,所以这两天就研究了一下,基本调通。现总结如下,以待查用。

一、geotrellis.slick 简介

geotrellis.slick是geotrellis的一个模块,它是对slick的封装。当然如果你要问我什么是geotrellis,请你先从底部的系列链接中看看前面的博客,大致能对其有个了解。

先介绍一下slick,它是一款开源的scala语言数据库处理框架,官网http://slick.lightbend.com/。官网介绍如下:

Slick is a modern database query and access library for Scala. It allows you to work with stored data almost as if you were using Scala collections while at the same time giving you full control over when a database access happens and which data is transferred. You can write your database queries in Scala instead of SQL, thus profiting from the static checking, compile-time safety and compositionality of Scala. Slick features an extensible query compiler which can generate code for different backends.

大概是说Slick使得我们能像处理普通Scala集合那样处理多种数据库,并能对数据库进行控制,相当于一个ORM框架。它支持以下几种数据库:

  • SQLServer 2008, 2012, 2014
  • Oracle 11g
  • DB2 10.5
  • MySQL
  • PostgreSQL
  • SQLite
  • Derby/JavaDB
  • HSQLDB/HyperSQL
  • H2

geotrellis.slick对其进行了封装,支持PostGIS数据库并能够简单的进行空间数据的读写。

二、geotrllis.slick 使用

2.1 引用

话不多说,直接进入干货。首先是对geotrllis.slick的引用,在build.sbt中的libraryDependencies添加如下项:

"org.locationtech.geotrellis" %% "geotrellis-slick" % 1.1.1

2.2 创建数据库连接类

与普通JDBC方式连接基本相同,创建一个连接对象即可。代码如下:

object ConnDatabase {
  def newInstance(pghost: String, pgdb: String, pguser: String, pgpass: String) = {
    val s = s"jdbc:postgresql://$pghost/$pgdb"
    Database.forURL(
      s,
      driver="org.postgresql.Driver",
      user=pguser,
      password=pgpass
    )
  }
}

trait ConnDatabase {
  protected var db: Database = null

  def connectDb(pghost: String, pgdb: String, pguser: String, pgpass: String) {
    db = ConnDatabase.newInstance(pghost, pgdb, pguser, pgpass)
  }
}

创建了一个特质(trait)ConnDatabase,其中包含了db对象,此对象即为数据库连接,后续都要基于此对象进行操作。

2.3 创建数据库表与实体类映射

首先要在PostGIS中创建一个数据库(此处假设为test),此数据库要选择空间模板以使该数据库支持空间操作。

在创建映射之前,需要先创建一个类使得程序能够正确识别此类映射并加入相应PostGIS扩展。代码如下:

object driver extends PostgresDriver with PostGisSupport {
  override val api = new API with PostGISAssistants with PostGISImplicits
}

此类中的api对象需要被实体类和操作类引用,具体在下面讲述。

我们以城市这个实体为例,假设仅仅关注城市名称以及经纬度坐标,考虑到数据库操作则需要再加一ID项。那么城市实体的定义如下:

import driver.api._

class City(tag: Tag) extends Table[(Int, String, Point)](tag, "cities") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

  def name = column[String]("name")

  def geom = column[Point]("geom")

  def * = (id, name, geom)
}

直观上说这段代码很容易理解,City实体对应与cities表;id字段对应表中id字段,并为主键及自动增长,类型为Int;name对应表中name字段,类型为String;geom对应空间字段geom,类型为Point(空间字段类型可以直接设置为Geometry);def * 表示三个字段的组合。当然此处也可以设置字段可空,只需要将类型使用Option包裹并且上下对应即可,如需要设置geom可空,则整个类修改如下:

class City(tag: Tag) extends Table[(Int, String, Option[Point])](tag, "cities") {
    def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

    def name = column[String]("name")

    def geom = column[Option[Point]]("geom")

    def * = (id, name, geom)
}

所以在定义实体类与数据库表映射的时候,首先引入上面driver中定义的api,之后定义实体类继承自Table对象,其泛型即为def *中组合类型,并且二者顺序必须完全一致。这样就定义好了二者映射。

2.4 操作类

上文讲到slick的优势就在于我们可以像使用scala集合那样读取数据库中信息,并能够对数据库进行操作。首先定义一个CityOperate类,在其中完成对City的操作,有点dal或者bll的感觉。

import geotrellis.postgis.model.City
import org.scalatest.concurrent.ScalaFutures
import geotrellis.vector._

object CityOperate extends ConnDatabase with ScalaFutures {
  import driver.api._
  implicit override val patienceConfig = PatienceConfig(timeout = Span(5, Seconds))
  val pguser = "******"
  val pgpass = "******"
  val pgdb = "test"
  val pghost = "127.0.0.1:5432"
  connectDb(pghost, pgdb, pguser, pgpass)
  val CityTable = TableQuery[City]
}

该类继承自ConnDatabase和ScalaFutures。

其中ConnDatabase是上文我们写好的数据库连接类,主要目的在于得到其中的db对象,所以必须先执行connectDb函数,传入数据库参数。

ScalaFutures主要是获取查询等的Future操作的结果值。

引入上面driver中定义的api,并重写patienceConfig加大超时时间,防止下面的future执行超时。

CityTable很明显是City的映射对象,主要基于此对象对数据库进行操作。

2.4.1 创建表

我们可以无需创建表cities而由slick完成,只需要在上述类中添加如下方法:

def createSchema {
    try {
      db.run(CityTable.schema.create).futureValue
    } catch {
      case _: Throwable =>
    }
}

该函数实现的功能就是创建cities表。从这段代码大致能看出slick的整个操作模式,其所有操作都要执行db.run函数,传入的是进行的操作,无论是增删改查还是创建、删除表等。此函数的结果需要进行futureValue操作,来获取真正的结果,如果不加此项则不会进行操作。CityTable.schema.create表示进行的是创建schema操作。

可以通过CityTable.schema.create.statements来查看创建表的SQL语句。

2.4.2 删除表

有了创建表操作,删除操作就很容易了,代码如下:

def dropSchema {
    try {
      db.run(CityTable.schema.drop).futureValue
    } catch {
      case _: Throwable =>
    }
  }

很简单,只需要在db.run函数中传入CityTable.schema.drop。

可以通过CityTable.schema.drop.statements来查看创建表的SQL语句。

2.4.3 增

进入数据库操作以及码农的最最最常规操作。增加数据代码如下:

def insertData(data: Array[(String, Point)]) {
    db.run(CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) })
}

函数接受(String, Point)类型的数组,表示名称和位置。插入操作也很容易,直接像db.run函数传入CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) },++=正是一个插入操作的action,前面表示的是要插入的字段名称,后面则是对应的数据,此处表示插入name和geom字段,后面为数据。

当然如果在实体映射中某个字段按照上述方式设置可空,那么在insert以及下面的update操作的时候此字段的类型都要为Option,即有值的地方使用Some包裹,无值的地方设置为None。

可以通过

(CityTable.map(c => (c.name, c.geom)) ++= data.map { d => (d._1, d._2) }).statements

来查看插入的SQL语句,其实到这里大家应该能总结出来规律,只要对传入db.run函数的参数执行statements操作就能查看此操作的SQL语句,以下同,不再赘述。

2.4.4 删

删除数据分为删除全部和有条件删除。

  • 删除全部数据:
def deleteAllData {
    val q = for { c <- CityTable } yield c
    db.run(q.delete).futureValue
}

从这段代码能看出slick对数据操作的基本流程,首先使用for循环生成想要处理的数据的集合,而后使用db.run对此集合执行相应的操作。

上述代码中q表示的是全部数据,db.run传入的也是q.delete,则表中所有数据都会被删除。

  • 删除部分数据:
def bboxBuffer(x: Double, y: Double, d: Double) =
    Polygon(Line(
      (x - d, y - d),
      (x - d, y + d),
      (x + d, y + d),
      (x + d, y - d),
      (x - d, y - d)
    ))
    
def deleteDataByBufer {
    val bbox = bboxBuffer(78.32, 40.30, 0.01)
    val q = for(c <- CityTable if c.geom @&& bbox) yield c
    db.run(q.delete).futureValue
}

其中bboxBuffer函数表示给定一个点和距离创建其缓冲区。

在deleteDataByBufer函数中,我们先创建了一个bbox缓冲区,该函数的目的是删除所有坐标在给定缓冲区内的城市。可以看出此处q的值在获取的时候稍有变化,加了一个c.geom @&& bbox的条件,@&&是geotrellis写好的空间支持函数,该函数表示前面的空间是否在缓冲区(Polygon)中。将q.delete传入db.run即可实现删除部分数据的目的,当然按照其他条件删除则同理。

2.4.5 改

def updateData(name: String) {
    val bbox = bboxBuffer(78.32, 40.30, 0.01)
    val q = for (c <- CityTable if c.geom @&& bbox) yield c.name
    db.run(q.update(name)).futureValue
}

此函数实现的功能是将词缓冲区内城市名称全部改为传入的name参数。区别只是在于将q.update(name)传入db.run函数。

2.4.6 查

同样查也分为查询全部数据和查询部分数据,其实基本与上述相同。

  • 查询全部数据:
def getData = {
    val q = for { c <- CityTable } yield (c.name, c.geom)
    db.run(q.result).futureValue.toList
}

q获取到的是城市名称和位置信息,则最后查询的结果就是所有城市的名称和位置信息,不包含id。将q.result传入db.run函数即可获取到最终结果。

  • 查询部分数据:
def getDataByBuffer = {
    val bbox = bboxBuffer(78.32, 40.30, 0.01)
    val q = for(c <- CityTable if c.geom @&& bbox) yield c
    db.run(q.result).futureValue.toList
}

该函数实现的功能是查询缓冲区内的城市信息,此处q直接获取到的是缓冲区内的城市所有信息,所以将q.result传入db.run后就能获取到缓冲区内的城市的所有信息。

  • 对数据进行空间操作:

geotrelis.slick支持将scala的空间操作转换为PostGIS的空间函数,如下:

def getGeomWKTData {
    val q = for {
        c <- CityTable
    } yield c.geom.asEWKT

    println(q.result.statements)
    db.run(q.result).futureValue.toList
}

上述函数中直接对geom对象进行asEWKT操作,将Point转化为WKT语言,并输出查询结果。执行上面的函数会打印出如下信息:

List(select ST_AsEWKT("geom") from "cities")

表明geotrellis.slick确实将asEWKT操作转换为PostGIS中的ST_AsEWKT函数。

三、总结

本文尝试了geotrliis.slick的相关功能和用法,由于刚接触可能有理解不透彻的地方,欢迎留言指正,不甚感激!

转载自:http://www.cnblogs.com/shoufengwei/p/7337087.html

You may also like...