当存储基于文档的 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 的手册找到其详细的语法。语法缩略如下:

1

2

3

4

5{

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。或许在其他的场景下,我的选择会不一样,时间会给我答案。我觉得,胸中有了这两个的组合,没有什么我做不了的了。

mongodb near java_简阅MongoDB JVM开发库相关推荐

  1. mongodb连接java_如何从Java EE无状态应用程序连接到MongoDB

    mongodb连接java 在本文中,我将介绍如何从无状态Java EE应用程序连接到MongoDB,以利用与MongoDB Java驱动程序提供的数据库的内置连接池. 如果您开发的REST API对 ...

  2. mongodb for java_【MongoDB for Java】Java操作MongoDB

    上一篇文章: http://www.cnblogs.com/hoojo/archive/2011/06/01/2066426.html介绍到了在MongoDB的控制台完成MongoDB的数据操作,通过 ...

  3. python mongodb orm_Django 通过 mongoengine 连接 MongoDB 进而使用orm进行CRUD

    一. 在python脚本中, 我们通常可以使用pymongo模块实现与mongodb数据库的交互, 但是在使用Django框架进行定制开发的web server 项目中, 仍然使用pymongo模块的 ...

  4. Windows下MongoDB的安装与设置MongoDB服务

    Mongo DB 是目前在IT行业非常流行的一种非关系型数据库(NoSql),其灵活的数据存储方式备受当前IT从业人员的青睐.Mongo DB很好的实现了面向对象的思想(OO思想),在Mongo DB ...

  5. 用python向mongodb插入数据_Python操作MongoDB数据库(一)

    Python操作MongoDB数据库(一) 前言 干货文章继续出发!隔的时间有些久了哈,对 MongoDB 安装回顾的同学,可以看下windows环境 <初识 MongoDB 数据库>.这 ...

  6. mongodb 搜索速度_初识 MongoDB 数据库

    初识 MongoDB 数据库 前言 Flask 基础框架在之前的三篇文章中写完了.想要学习 web 相关的同学可以自己回顾翻一下,相信看完了,你也可以写出来一个简单的小案例来炫耀一波! 说到 web ...

  7. mongodb 数组添加_NoSQL之MongoDB——简介

    MongoDB是一种开源文档型数据库,它具有高性能,高可用性,自动扩展性 1.文档数据库 MongoDB用一个文档来表示一条记录,文档的数据结构由键值对组成.MongoDB文档类似于JSON对象,字段 ...

  8. 给服务器mongodb设置权限_认识 MongoDB

    大部分数据库都是 C/S 架构,服务端提供服务,操作时需要使用客户端. MongoDB 也是如此,需要启动服务端进程 mongod 提供服务,然后使用其自带的 mongo 命令行程序或者其它客户端与服 ...

  9. MongoDB入门(1)- MongoDB简介

    什么是MongoDB NoSQL NoSQL systems are also sometimes called "Not only SQL" to emphasize that ...

最新文章

  1. 2021中科院院士候选名单出炉:清华胡事民、南大周志华等人在列
  2. copy一下面试题目
  3. php顺序、二分查找
  4. 预备作业03 20162320刘先润
  5. Grafana 使用教程 --- 开源的度量分析与可视化套件
  6. 关于AD域账户和组的管理技巧
  7. 敏捷团队为何失败,Bash技巧,Emacs vs. Vim,为Linux粉丝撰写的12部小说读物,以及其他热门歌曲
  8. CentOS 6 下升级安装Mysql 5.5 完整步骤
  9. math api matrix
  10. 服务 23 年,苹果宣布停止 macOS Server
  11. redis 条件查询
  12. linux安装vmware出现Gtk-Message: Failed to load module pk-gtk-module canberra-gtk-module的解决方法...
  13. 5.Hbase API 操作开发
  14. css文件更新后浏览器网页仍然显示以前的页面解决方法
  15. 小程序电子名片免费制作
  16. vmare下Linux空间扩大教程(chinaitlab)
  17. tplink怎么进去_怎么进入TP-Link路由器设置界面? | 192路由网
  18. A Hierarchical Latent Variable Encoder-Decoder Model for Generating Dialogues论文笔记
  19. 测试类型(αβ测试 、AB测试)
  20. 麒麟子Cocos Creator 3D研究笔记十一:实用Shader之单张纹理实现武器动态发光

热门文章

  1. c语言图像系统,JPEG2000核心编码C语言实现及图像验证系统设计
  2. 创业早期容易犯哪些错误?
  3. 【PHP MySQL】数据库专题 第二课 创建数据库 表
  4. 趋易避难,人之本性!
  5. 5.Python函数高级
  6. java设计五子棋_JAVA 五子棋设计
  7. 【算法编程】小学数学题难倒博士
  8. C++ 类模板和模板类
  9. MyCat实现MySQL读写分离(双主双从多库)
  10. SOLIDWORKS中的两个基本概念