簡閱MongoDB JVM開發庫

jopen 9年前發布 | 26K 次閱讀 MongoDB NoSQL數據庫

當存儲基于文檔的 JSON 數據的時候,MongoDB 是我最喜歡的數據庫。基于 JVM 的語言在與 MongoDB 交互上有很多種選擇。我覺得拿四個最流行的解決方案并且都實現一個用例,對我來說不失為一個好的練習。用例:創建一個可以獲取一個城市和距其最近的城市的列表的 REST 服務。

我要比較的四個選擇是:標準的MongoDB Java Driver、Jongo、Monphia和Spring Data Mongo。為了簡潔,我是用 groovy 完成代碼,并且使用 Spring Boot 以減少樣板代碼。

基礎配置

Spring Boot 應用的代碼非常簡潔,如下:

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
@EnableAutoConfiguration
@ComponentScan
@Configuration
class MongoComparison
{
    static void main(String[] args) {
        SpringApplication.run(MongoComparison, args);
    }
}

同時,我也提供了此次對比所使用的Gradle構建文件:

buildscript {
    repositories {
        jcenter()
        maven {
            url 'http://repo.spring.io/milestone'
        }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.9.RELEASE")
    }
}
apply plugin: 'groovy'
apply plugin: 'spring-boot'
repositories {
    jcenter()
    maven { url 'http://repo.spring.io/milestone' }
    maven { url 'http://www.allanbank.com/repo/' }
}
dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-data-mongodb")
    compile("org.jongo:jongo:1.1")
    compile("org.mongodb.morphia:morphia:0.108")
    compile("de.grundid.opendatalab:geojson-jackson:1.2")
    compile("org.codehaus.groovy:groovy-all:2.3.6")
 }
task wrapper(type: Wrapper) {
    gradleVersion = '2.1'
}

因為我使用了 Spring Boot 和 Spring Data MongoDB 框架,一些配置可以忽略。例如,Spring Boot 框架在為 Spring Boot 的應用程序上下文提供了 MongoClient bean 和 MongoTemplate bean。你無需在你的配置文件中額外配置(我是用的是 YAML 風格的配置)。

spring:
    groovy:
        template:
            check-template-location: false
    data:
        mongodb:
            host: "localhost"
            database: "citydbdata"

基本框架完成之后,我們可以開始對比。

MongoDB Java驅動

因為所有的連接 MongoDB 的程序,都用到了 MongoDB 原生的 Java 驅動,所以我覺得從 MongoDB Java Driver (下稱 Java Driver)開始最合適。Java Driver 是 JVM 上使用 MongoDB 的最底層的途徑。也就是說,寫出的程序會略顯冗長,并且API不如其他的更加用戶友好。然而,你使用Java Driver能夠實現所有的功能。Java Driver 在 Spring Data MongoDB 里已經自動引入,如果你需要單獨使用的話,需要引入相應的依賴。

這是使用Java Driver實現的代碼:

import com.mongodb.*
import org.bson.types.ObjectId
import org.geojson.Point
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
import javax.annotation.PostConstruct
import static org.springframework.web.bind.annotation.RequestMethod.GET
@RestController
@RequestMapping("/mongoclient")
class CityControllerMongoClient {
    final DB db
    def dbObjectToCityTransformer = { DBObject it ->
        def objectMap = it.toMap()
        return new City(_id: objectMap._id, name: objectMap.name, location: new Point(objectMap.location.coordinates[0], objectMap.location.coordinates[1]))
    }
    @Autowired
    CityControllerMongoClient(MongoClient mongoClient) {
        db = mongoClient.getDB("citydbmongoclient")
    }
    @RequestMapping(value="/", method = GET)
    List<City> index() {
        return db.getCollection("city").find().collect(dbObjectToCityTransformer)
    }
    @RequestMapping(value="/near/{cityName}", method = GET)
    ResponseEntity nearCity(@PathVariable String cityName) {
        def city = dbObjectToCityTransformer(db.getCollection("city").findOne(new BasicDBObject("name", cityName)))
        if(city) {
            def point = new BasicDBObject([type: "Point", coordinates: [city.location.coordinates.longitude, city.location.coordinates.latitude]])
            def geoNearCommand =  new BasicDBObject([geoNear: "city", spherical: true, near: point])
            def closestCities = db.command(geoNearCommand).toMap()
            def closest = closestCities.results[1]
            return new ResponseEntity([name:closest.obj.name, distance:closest.dis/1000], HttpStatus.OK)
        }
        else {
            return new ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }
    @PostConstruct
    void populateCities() {
        db.getCollection("city").drop()
        [new City(name: "London",
                location: new Point(-0.125487, 51.508515)),
         new City(name: "Paris",
                 location: new Point(2.352222, 48.856614)),
         new City(name: "New York",
                 location: new Point(-74.005973, 40.714353)),
         new City(name: "San Francisco",
                 location: new Point(-122.419416, 37.774929))].each {
            DBObject location = new BasicDBObject([type: "Point", coordinates: [it.location.coordinates.longitude, it.location.coordinates.latitude]])
            DBObject city = new BasicDBObject([name: it.name, location: location])
            db.getCollection("city").insert(city)
        }
        db.getCollection("city").createIndex(new BasicDBObject("location", "2dsphere"))
    }
    static class City {
        ObjectId _id
        String name
        Point location
    }
}

Java Driver 整體以 DBObject 為中心,你需要一直提供領域對象和DBObject之間的映射。Java Driver沒有提供任何形式的對象映射。幸運的是, DBObject 的結構很像 map,并且 Groovy Map 的簡潔的風格讓其操作起來方便許多。本例中,要找到距某城市最近的城市以及其最短距離時,需要用到 geoNear 命令,你可能需要從 mongoDB 的手冊找到其詳細的語法。語法縮略如下:

{
   geoNear: collectionName,
   near: { type: "Point" , coordinates: [ longitude, latitude ] } ,
   spherical: true
}

geoNear 命令會返回集合中距離最近的對象,并且提供一個字段來標識他們之間的距離;距離的單位是米。 geoNear 命令中的near字段的格式有兩種,一種是上面代碼示例,另一種是更傳統的2個 double 值組成的數組。因為前一種符合 GeoJSON 的標準,我更推薦這種方式。在我所有的例子中,我盡量都是用 GeoJSON 記法來存儲地理位置信息數據。從代碼里能看出來,我使用了一個提供了所有 GeoJSON 類型支持的 Java 類庫。

撇開所有 DBObject 到領域對象的約定,例子中的代碼都非常易讀。當然你需要知道 MongoDB 查詢的細節;然而當你了解了之后,Java Driver 就是一個非常強大的工具。

Jongo

Jongo 框架支持基于字符串的交互和查詢(查詢時不需要創建 DBObject ),因此允許你使用接近于 Mongo Shell 的方式與 MongoDB 實例進行交互。Jongo 使用 Jackson 框架來完成對象映射,所以無需將查詢結果和想插入的數據轉換為 DBObject 實例。我在使用的 GeoJSON 庫內置了Jackson 的支持,對此,我們無需為此多編寫代碼。

Jongo的用例的代碼如下:

import com.fasterxml.jackson.databind.ObjectMapper
import com.mongodb.MongoClient
import org.bson.types.ObjectId
import org.geojson.Point
import org.jongo.Jongo
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
import javax.annotation.PostConstruct
import static org.springframework.web.bind.annotation.RequestMethod.GET
@RestController
@RequestMapping("/jongo")
class CityControllerJongo {
    final Jongo jongo
    @Autowired
    CityControllerJongo(MongoClient mongoClient) {
        jongo = new Jongo(mongoClient.getDB("citydbjongo"))
    }
    @RequestMapping(value="/", method = GET)
    List<City> index() {
        return jongo.getCollection("city").find().as(City).asList()
    }
    @RequestMapping(value="/near/{cityName}", method = GET)
    ResponseEntity nearCity(@PathVariable String cityName) {
        def city = jongo.getCollection("city").findOne("{name:'$cityName'}").as(City)
        if(city) {
            def command = """{
                geoNear: "city",
                near: ${new ObjectMapper().writeValueAsString(city.location)},
                spherical: true
            }"""
            def closestCities = jongo.runCommand(command).as(GeoNearResult) as GeoNearResult<City>
            def closest = closestCities.results[1]
            return new ResponseEntity([name:closest.obj.name, distance:closest.dis/1000], HttpStatus.OK)
        }
        else {
            return new ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }
    @PostConstruct
    void populateCities() {
        jongo.getCollection("city").drop()
        [ new City( name:"London",
                location: new Point(-0.125487, 51.508515)),
          new City( name:"Paris",
                  location: new Point(2.352222, 48.856614)),
          new City( name:"New York",
                  location: new Point(-74.005973, 40.714353)),
          new City( name:"San Francisco",
                  location: new Point(-122.419416, 37.774929)) ].each {
            jongo.getCollection("city").save(it)
        }
        jongo.getCollection("city").ensureIndex("{location:'2dsphere'}")
    }
    static class GeoNearResult<O> {
        List<GeoNearItem<O>> results
    }
    static class GeoNearItem<O> {
        Double dis
        O obj
    }
    static class City {
        ObjectId _id
        String name
        Point location
    }
}

從例子中可以看出,Jongo 更面向字符串,尤其是使用 GeoNear 命令查詢的時候。同時,多虧 Jackson 框架,我們查詢和插入時,不用編寫任何的轉換的代碼。

如果你是先接觸到MongoDB,熟悉shell命令并且不想做手工映射的話,Jongo是非常便捷的。但是,你需要去了解Mongo Shell API的確切語法;同時,你在構造查詢、編寫命令式沒有自動的代碼補全,如果你覺得這樣是可以接受的話,Jongo是一個不錯的選擇。

Morphia

MongoDB 的開發者(因為Trisha Gee,我不能說漢子們)為MongoDB量身定做了一個映射框架。 Morphia是一個注解驅動的框架,也就是說為了使用 Morphia ,你得使用注解來注釋你的 POJO (盡管如此,你可以不寫注解以使用默認的注解)。 Morphia 支持 MongoDB 的大部分函數,遺憾的是沒有對 GeoJSON 提供支持從而也不支持 geoNear。MongoDB 的開發者專注的開發 MongoDB Java Driver 3.0,有些忽略 Morphia。 可能會在未來的版本中提供對 GeoJSON 的支持。

因為我用到了 geoNear 函數,除了把Java Driver的測試用例中的代碼中的拿來復用也沒有更好的選項了。如下是用 Morphia 實現的用例:

import com.mongodb.*
import org.bson.types.ObjectId
import org.geojson.Point
import org.mongodb.morphia.*
import org.mongodb.morphia.annotations.*
import org.mongodb.morphia.converters.TypeConverter
import org.mongodb.morphia.mapping.MappedField
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
import javax.annotation.PostConstruct
import static org.springframework.web.bind.annotation.RequestMethod.GET
@RestController
@RequestMapping("/mongomorphia")
class CityControllerMorphia {
    final Datastore datastore
    @Autowired
    CityControllerMorphia(MongoClient mongoClient) {
        def morphia = new Morphia()
        morphia.mapper.converters.addConverter(GeoJsonPointTypeConverter)
        datastore = morphia.createDatastore(mongoClient, "citymorphia")
    }
    @RequestMapping(value="/", method = GET)
    List<City> index() {
        return datastore.find(City).asList()
    }
    @RequestMapping(value="/near/{cityName}", method = GET)
    ResponseEntity nearCity(@PathVariable String cityName) {
        def city = datastore.find(City, "name", cityName).get()
        if(city) {
            def point = new BasicDBObject([type: "Point", coordinates: [city.location.coordinates.longitude, city.location.coordinates.latitude]])
            def geoNearCommand =  new BasicDBObject([geoNear: "City", spherical: true, near: point])
            def closestCities = datastore.DB.command(geoNearCommand).toMap()
            def closest = (closestCities.results as List<Map>).get(1)
            return new ResponseEntity([name:closest.obj.name, distance:closest.dis/1000], HttpStatus.OK)
        }
        else {
            return new ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }
    @PostConstruct
    void populateCities() {
        datastore.delete(datastore.createQuery(City))
        [new City(name: "London",
                location: new Point(-0.125487, 51.508515)),
         new City(name: "Paris",
                 location: new Point(2.352222, 48.856614)),
         new City(name: "New York",
                 location: new Point(-74.005973, 40.714353)),
         new City(name: "San Francisco",
                 location: new Point(-122.419416, 37.774929))].each {
            datastore.save(it)
        }
        datastore.getCollection(City).createIndex(new BasicDBObject("location", "2dsphere"))
    }
    @Entity
    static class City {
        @Id
        ObjectId id
        String name
        Point location
    }
    static class GeoJsonPointTypeConverter extends TypeConverter {
        GeoJsonPointTypeConverter() {
            super(Point)
        }
        @Override
        Object decode(Class<?> targetClass, Object fromDBObject, MappedField optionalExtraInfo) {
            double[] coordinates = (fromDBObject as DBObject).get("coordinates")
            return new Point(coordinates[0], coordinates[1])
        }
        @Override
        Object encode(Object value, MappedField optionalExtraInfo) {
            def point = value as Point
            return new BasicDBObject([type:"Point", coordinates:[point.coordinates.longitude, point.coordinates.latitude]])
        }
    }
}

因為 Morphia 框架沒有對 GeoJSON 提供支持,所以,你要么使用傳統的用包含兩個坐標的 double 類型的數組,要么寫你自己的轉換器。我選擇了后者,畢竟也不是那么難寫。不要忘了把你的轉換器加入到 Morphia 中。從代碼中可以看出,我已經使用 Morphia 的注解注釋了 City 類,對于那些熟悉 JPA 的開發者來說,這種方式直截了當。同時,因為 Morphia 不提供 2dsphere index 查詢支持,你要自己創建 2dsphere 索引。

針對MongoDB的Spring Data

最后但同樣重要的是 Spring Data,我在研究如何用它完成這個用例。如果你熟知 Spring Data 框架的話,你需要寫與數據存儲交互的庫接口,使用方法名來指定你需要使用的查詢。在此例中,我們只需要兩個查詢:根據名字查詢城市,找到距某城市最近的城市。Spring Data 框架支持 geospatial 查詢(地理空間查詢)。

Spring Data 框架有用來表示地理空間坐標的類,但是和 GeoJSON 不兼容(再提一遍:Spring 有其自有的方式)。所幸 Spring Data 能夠自動生成索引并且 mongo 也能夠處理 Spring Data 使用的坐標表示方式。

這是針對Morphia的實現:譯注:我認為是原文錯誤

import org.bson.types.ObjectId
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.geo.*
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.data.mongodb.core.index.*
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.MongoRepository
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import javax.annotation.PostConstruct
import static org.springframework.web.bind.annotation.RequestMethod.GET
@RestController
@RequestMapping("/mongodata")
class CityControllerMongoData {
    final CityRepository cityRepository
    @Autowired
    CityControllerMongoData(CityRepository cityRepository) {
        this.cityRepository = cityRepository
    }
    @RequestMapping(value="/", method = GET)
    List<City> index() {
        return cityRepository.findAll()
    }
    @RequestMapping(value="/near/{cityName}", method = GET)
    ResponseEntity nearCity(@PathVariable String cityName) {
        def city = cityRepository.findByName(cityName)
        if(city) {
            GeoResults<City> closestCities = cityRepository.findByLocationNear(city.location, new Distance(10000, Metrics.KILOMETERS))
            def closest = closestCities.content.get(1)
            return new ResponseEntity([name:closest.content.name, distance:closest.distance.in(Metrics.KILOMETERS).value], HttpStatus.OK)
        }
        else {
            return new ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }
    @PostConstruct
    void populateCities() {
        cityRepository.deleteAll()
        [ new City( name:"London",
                location: new Point(-0.125487, 51.508515)),
          new City( name:"Paris",
                  location: new Point(2.352222, 48.856614)),
          new City( name:"New York",
                  location: new Point(-74.005973, 40.714353)),
          new City( name:"San Francisco",
                  location: new Point(-122.419416, 37.774929)) ].each {
            cityRepository.save(it)
        }
    }
    @Document(collection = "city")
    static class City {
        ObjectId id
        String name
        @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
        Point location
    }
}
interface CityRepository extends MongoRepository<CityControllerMongoData.City, ObjectId> {
    CityControllerMongoData.City findByName(String name);
    GeoResults<CityControllerMongoData.City> findByLocationNear(Point point, Distance distance);
}

就可讀性來說,Spring Data 無疑是最好的。你不需要知道 MongoDB 里的查詢語句是如何構建的,你只需要使用庫中的命名慣例就好。當你使用2dsphere 索引的時候,要記住一點,就是使用 near query 方法時要加入 distance 參數,不然 Spring Data 在查詢 MongoDB 時會忽略 sphere 選項(在此情況下會失敗?報錯)。如果你不需要距離,你可以把命令的返回值城市對象的列表。你不用實現相應的接口,Spring Data 框架會替你實現。

使用Spring Data MongoDB 框架的時候,你也可以使用 MongoTemplate 類。MongoTemplate 類提供了一種類似于 Jongo 和 Java Driver 的機制。使用 MongoTemplate 可以很容易的實現geoNear 查詢。

你也可能已經注意到,Spring Data MongoDB 是唯一一個沒有在Java代碼中提供數據庫名字的框架。這是因為Spring Data使用MongoTemplate,而 MongoTemplate 需要你在配置文件中配置。也就是說,你可以將數據庫的名字注入到對應的變量上,并且使用此變量來代表數據庫的名字。

對于Spring Data Mongo唯一不滿意的地方就是他們選取了一種不標準的方式來表示地理信息數據。如果你有一個mongo的集合,集合里全是使用GeoJSON格式化過的數據,因為倉庫不能處理,基本上你就“完蛋”了(至少生成的near查詢不行)。我嘗試了在我映射的City對象里使用GeoJSON的類,但是不能使轉換正常工作(Spring Data框架沒有使用Jackson框架做序列化)。并且,庫接口里的geoNear方法生成的query串使用的舊的坐標對,而不是GeoJSON幾何結構。如果Spring Data能夠提供對GeoJSON格式的位置和查詢的支持,就像在一個很好的蛋糕頂端添加了一顆櫻桃。

結論

對于這個用例,我對于框架選擇是:Spring Data,接著是Jongo和Java Driver。 Jongo排第二是因為其映射的功能,而不是其他方面的功能;Java Driver也基本相同。Morphia排最后是因為缺少對geoNear查詢的支持,并且缺少對地理對象的內置支持(double類型的數組除外)。當Morphia的下一個版本發布的時候,它的關注點可能會改變。使用Java Driver寫出的程序可能較冗長,但是和Groovy搭配在一起使用,冗長的缺點也可以克服。

這是一個相當簡單的例子,但是對我來說這是一個寶貴的學習經驗。基于上文中我的了解,我可能傾向于使用Spring Data MongoDB框架,并且當在庫接口里函數過于復雜時,我會引入Java Driver。或許在其他的場景下,我的選擇會不一樣,時間會給我答案。我覺得,胸中有了這兩個的組合,沒有什么我做不了的了。

原文鏈接: dzone 翻譯: ImportNew.com - mingyuan
譯文鏈接: http://www.importnew.com/15958.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!