简阅MongoDB JVM开发库

jopen 9年前

当存储基于文档的 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