使用 Kotlin、Spring Boot 和 PostgreSQL 的地理空間訊息應用程式

工程 | Sébastien Deleuze | 2016 年 3 月 20 日 | ...

繼我的第一篇 Kotlin 部落格文章之後,今天我想介紹我為即將到來的 Spring I/O 2016 會議 所開發的新的 Spring Boot + Kotlin 應用程式,主題是 "使用 Kotlin 和 Spring Boot 開發地理空間 Web 服務"。

處理原生資料庫功能

此應用程式的目標之一是了解如何利用原生資料庫功能,就像我們在 NoSQL 世界中所做的一樣。在這裡,我們想使用 PostGIS 提供的地理空間支援,它是 PostgreSQL 的空間資料庫擴充套件。原生 JSON 支援 也可能是一個很好的用例。

這個地理空間訊息應用程式範例在 GitHub 上可用,有 2 種版本

一個 Spring Data JPA + Hibernate Spatial 變體會很有趣,所以歡迎通過 pull request 貢獻它 ;-) Kotlin Query DSL 支援也會很好,但目前不支援(如果您感興趣,請在 這個 issue 上發表評論)。在這篇部落格文章中,我將重點介紹 Exposed 變體。

地理空間訊息應用程式碼導覽

網域模型

由於這兩個 Kotlin 類別,我們的網域模型可以很容易地描述

class Message(
    var content  : String,
    var author   : String,
    var location : Point? = null,
    var id       : Int?   = null
)

class User(
    var userName  : String,
    var firstName : String,
    var lastName  : String,
    var location  : Point? = null
)

SQL 結構描述

Exposed 允許我們使用型別安全的 SQL API 來描述表格的結構,這非常方便使用(自動完成、重構和不易出錯)

object Messages : Table() {
    val id       = integer("id").autoIncrement().primaryKey()
    val content  = text("content")
    val author   = reference("author", Users.userName)
    val location = point("location").nullable()
}

object Users : Table() {
    val userName  = text("user_name").primaryKey()
    val firstName = text("first_name")
    val lastName  = text("last_name")
    val location  = point("location").nullable()
}

有趣的是,Exposed 本身不支援 PostGIS 功能,例如幾何類型或地理空間請求。這就是 Kotlin 擴展 發光的地方,並且允許通過幾行程式碼添加此類支援,而無需使用擴展類別

fun Table.point(name: String, srid: Int = 4326): Column<Point>
  = registerColumn(name, PointColumnType())

infix fun ExpressionWithColumnType<*>.within(box: PGbox2d) : Op<Boolean>
  = WithinOp(this, box)

儲存庫

更新:我們現在可以使用 Exposed @Transactional 支援!事務管理只需使用 @EnableTransactionManagement 註解和 Application 類別中的 PlatformTransactionManager bean 進行配置。

我們的儲存庫也非常短且非常靈活,因為它們允許您使用型別安全的 SQL API 撰寫任何類型的 SQL 請求,即使是複雜的 WHERE 子句。

請注意,由於我們使用的是 Spring Framework 4.3,因此我們 不再需要在這種單一建構子的類別中指定 @Autowired 註解

interface CrudRepository<T, K> {
    fun createTable()
    fun create(m: T): T
    fun findAll(): Iterable<T>
    fun deleteAll(): Int
    fun findByBoundingBox(box: PGbox2d): Iterable<T>
    fun updateLocation(userName:K, location: Point)
}

interface UserRepository: CrudRepository<User, String>

@Repository
@Transactional // Should be at @Service level in real applications
class DefaultUserRepository(val db: Database) : UserRepository {

    override fun createTable() = SchemaUtils.create(Users)

    override fun create(user: User): User {
        Users.insert(toRow(user))
        return user
    }

    override fun updateLocation(userName:String, location: Point) = {
        location.srid = 4326
        Users.update({Users.userName eq userName})
            { it[Users.location] = location }
    }

    override fun findAll() = Users.selectAll().map { fromRow(it) }

    override fun findByBoundingBox(box: PGbox2d) =
            Users.select { Users.location within box }
                 .map { fromRow(it) }

    override fun deleteAll() = Users.deleteAll()

    private fun toRow(u: User): Users.(UpdateBuilder<*>) -> Unit = {
        it[userName] = u.userName
        it[firstName] = u.firstName
        it[lastName] = u.lastName
        it[location] = u.location
    }

    private fun fromRow(r: ResultRow) =
        User(r[Users.userName],
             r[Users.firstName],
             r[Users.lastName],
             r[Users.location])
}

控制器

控制器也很簡潔,並使用 Spring Framework 4.3 即將推出的 @GetMapping / @PostMapping 註解,它們只是 @RequestMapping 註解的特定方法快捷方式

@RestController
@RequestMapping("/user")
class UserController(val repo: UserRepository) {

    @PostMapping
    @ResponseStatus(CREATED)
    fun create(@RequestBody u: User) { repo.create(u) }

    @GetMapping
    fun list() = repo.findAll()

    @GetMapping("/bbox/{xMin},{yMin},{xMax},{yMax}")
    fun findByBoundingBox(@PathVariable xMin:Double,
                          @PathVariable yMin:Double,
                          @PathVariable xMax:Double,
                          @PathVariable yMax:Double)
            = repo.findByBoundingBox(
                        PGbox2d(Point(xMin, yMin), Point(xMax, yMax)))

    @PutMapping("/{userName}/location/{x},{y}")
    @ResponseStatus(NO_CONTENT)
    fun updateLocation(@PathVariable userName:String,
                       @PathVariable x: Double,
                       @PathVariable y: Double)
            = repo.updateLocation(userName, Point(x, y))
}

客戶端是一個純 HTML + Javascript 應用程式,使用 OpenLayers 地圖函式庫開發(請參閱 index.htmlmap.js 了解更多詳細資訊),它可以對您進行地理定位,並創建透過 Server-Sent Events 發送/接收到/來自其他使用者的地理定位訊息。

Screenshot

最後但並非最不重要的一點是,由於出色的 Spring REST docs 專案,REST API 經過了完整的測試和記錄,請參閱 MessageControllerTestsindex.adoc 了解更多詳細資訊。

結論

我開發此應用程式的主要印象是,它既有趣又高效,並且具有 SQL API 和 Kotlin 型別系統以及 空值安全 提供的高度靈活性和安全性。產生的 Spring Boot 應用程式是一個 18 MB 的獨立可執行 jar,記憶體消耗低(該應用程式可以在 -Xmx32m 下執行!!!)。使用 Spring REST docs 也是一種樂趣,再次證明了 Kotlin 出色的 Java 互操作性。

我遇到的一些痛點(陣列註解屬性Java 8 Stream 支援完整的可呼叫參考支援)計劃在 Kotlin 1.1 中修復。Exposed 函式庫還很年輕,需要成熟,但從我的角度來看,它很有前景,並展示了 Kotlin 如何用於建構型別安全的 DSL API(這個 HTML 型別安全的建構器也是一個很好的例子)。

請記住,Spring Data 專案 在官方支援下,與 Kotlin 配合良好,如我在 之前的部落格文章中的 spring-boot-kotlin-demo 專案所示。

如果您剛好五月中旬在巴塞隆納(無論如何,去巴塞隆納總是個好時機!),千萬別錯過參加 Spring I/O 會議的機會。此外,SpringOne Platform(八月初,拉斯維加斯)的註冊也已於近期開放,如果您想享有早鳥票價,請把握機會。後者也仍在徵求演講提案。所以,如果您有興趣發表關於 Spring 或 Pivotal 相關技術的演講,歡迎提交!

取得 Spring 電子報

訂閱 Spring 電子報,掌握最新資訊

訂閱

搶先一步

VMware 提供培訓和認證,加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部