上一节,我们做的那个例子有点太简单了,通常的后台都会涉及一些数据库的操作,然后在暴露的API中提供处理后的数据给客户端使用。那么这一节我们要做的是集成MongoDB ( https://www.mongodb.com )。
MongoDB是什么?
MongoDB是一个NoSQL数据库,是NoSQL中的一个分支:文档数据库。和传统的关系型数据库比如Oracle、SQLServer和MySQL等有很大的不同。传统的关系型数据库(RDBMS)已经成为数据库的代名词超过20多年了。对于大多数开发者来说,关系型数据库是比较好理解的,表这种结构和SQL这种标准化查询语言毕竟是很大一部分开发者已有的技能。那么为什么又搞出来了这个什么劳什子NoSQL,而且看上去NoSQL数据库正在飞快的占领市场。
NoSQL的应用场景是什么?
假设说我们现在要构建一个论坛,用户可以发布帖子(帖子内容包括文本、视频、音频和图片等)。那么我们可以画出一个下图的表关系结构。
论坛的简略ER图
这种情况下我们想一下这样一个帖子的结构怎么在页面中显示,如果我们希望显示帖子的文字,以及关联的图片、音频、视频、用户评论、赞和用户的信息的话,我们需要关联八个表取得自己想要的数据。如果我们有这样的帖子列表,而且是随着用户的滚动动态加载,同时需要监听是否有新内容的产生。这样一个任务我们需要太多这种复杂的查询了。
NoSQL解决这类问题的思路是,干脆抛弃传统的表结构,你不是帖子有一个结构关系吗,那我就直接存储和传输一个这样的数据给你,像下面那样。
{ "id":"5894a12f-dae1-5ab0-5761-1371ba4f703e", "title":"2017年的Spring发展方向", "date":"2017-01-21", "body":"这篇文章主要探讨如何利用Spring Boot集成NoSQL", "createdBy":User, "images":["http://dev.local/myfirstimage.png","http://dev.local/mysecondimage.png"], "videos":[ {"url":"http://dev.local/myfirstvideo.mp4", "title":"The first video"}, {"url":"http://dev.local/mysecondvideo.mp4", "title":"The second video"} ], "audios":[ {"url":"http://dev.local/myfirstaudio.mp3", "title":"The first audio"}, {"url":"http://dev.local/mysecondaudio.mp3", "title":"The second audio"} ] }</div>
NoSQL一般情况下是没有Schema这个概念的,这也给开发带来较大的自由度。因为在关系型数据库中,一旦Schema确定,以后更改Schema,维护Schema是很麻烦的一件事。但反过来说Schema对于维护数据的完整性是非常必要的。
一般来说,如果你在做一个Web、物联网等类型的项目,你应该考虑使用NoSQL。如果你要面对的是一个对数据的完整性、事务处理等有严格要求的环境(比如财务系统),你应该考虑关系型数据库。
在Spring中集成MongoDB
在我们刚刚的项目中集成MongoDB简单到令人发指,只有三个步骤:
在 build.gradle 中更改 compile('org.springframework.boot:spring-boot-starter-web') 为 compile("org.springframework.boot:spring-boot-starter-data-rest")
在 Todo.java 中给 private String id; 之前加一个元数据修饰 @Id 以便让Spring知道这个Id就是数据库中的Id
新建一个如下的 TodoRepository.java
package dev.local.todo; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource(collectionResourceRel = "todo", path = "todo") public interface TodoRepository extends MongoRepository<Todo, String>{ } 此时我们甚至不需要Controller了,所以暂时注释掉 TodoController.java 中的代码。然后我们 ./gradlew bootRun 启动应用。访问 http://localhost:8080/todo 我们会得到下面的的结果。 { _embedded: { todo: [ ] }, _links: { self: { href: "http://localhost:8080/todo" }, profile: { href: "http://localhost:8080/profile/todo" } }, page: { size: 20, totalElements: 0, totalPages: 0, number: 0 } }</div>
我勒个去,不光是有数据集的返回结果 todo: [ ] ,还附赠了一个links对象和page对象。如果你了解 Hypermedia 的概念,就会发现这是个符合 Hypermedia REST API返回的数据。
说两句关于 MongoRepository<Todo, String>
这个接口,前一个参数类型是领域对象类型,后一个指定该领域对象的Id类型。
Hypermedia REST
简单说两句Hypermedia是什么。简单来说它是可以让客户端清晰的知道自己可以做什么,而无需依赖服务器端指示你做什么。原理呢,也很简单,通过返回的结果中包括不仅是数据本身,也包括指向相关资源的链接。拿上面的例子来说(虽然这种默认状态生成的东西不是很有代表性):links中有一个profiles,我们看看这个profile的链接 http://localhost:8080/profile/todo 执行的结果是什么:
{ "alps" : { "version" : "1.0", "descriptors" : [ { "id" : "todo-representation", "href" : "http://localhost:8080/profile/todo", "descriptors" : [ { "name" : "desc", "type" : "SEMANTIC" }, { "name" : "completed", "type" : "SEMANTIC" } ] }, { "id" : "create-todo", "name" : "todo", "type" : "UNSAFE", "rt" : "#todo-representation" }, { "id" : "get-todo", "name" : "todo", "type" : "SAFE", "rt" : "#todo-representation", "descriptors" : [ { "name" : "page", "doc" : { "value" : "The page to return.", "format" : "TEXT" }, "type" : "SEMANTIC" }, { "name" : "size", "doc" : { "value" : "The size of the page to return.", "format" : "TEXT" }, "type" : "SEMANTIC" }, { "name" : "sort", "doc" : { "value" : "The sorting criteria to use to calculate the content of the page.", "format" : "TEXT" }, "type" : "SEMANTIC" } ] }, { "id" : "patch-todo", "name" : "todo", "type" : "UNSAFE", "rt" : "#todo-representation" }, { "id" : "update-todo", "name" : "todo", "type" : "IDEMPOTENT", "rt" : "#todo-representation" }, { "id" : "delete-todo", "name" : "todo", "type" : "IDEMPOTENT", "rt" : "#todo-representation" }, { "id" : "get-todo", "name" : "todo", "type" : "SAFE", "rt" : "#todo-representation" } ] } }</div>
这个对象虽然我们暂时不是完全的理解,但大致可以猜出来,这个是todo这个REST API的元数据描述,告诉我们这个API中定义了哪些操作和接受哪些参数等等。我们可以看到todo这个API有增删改查等对应功能。
其实呢,Spring是使用了一个叫 ALPS (http://alps.io/spec/index.html) 的专门描述应用语义的数据格式。摘出下面这一小段来分析一下,这个描述了一个get方法,类型是 SAFE 表明这个操作不会对系统状态产生影响(因为只是查询),而且这个操作返回的结果格式定义在 todo-representation 中了。 todo-representation
{ "id" : "get-todo", "name" : "todo", "type" : "SAFE", "rt" : "#todo-representation" }</div>
还是不太理解?没关系,我们再来做一个实验,启动 PostMan (不知道的同学,可以去Chrome应用商店中搜索下载)。我们用Postman构建一个POST请求:
用Postman构建一个POST请求添加一个Todo
执行后的结果如下,我们可以看到返回的links中包括了刚刚新增的Todo的link http://localhost:8080/todo/588a01abc5d0e23873d4c1b8 ( 588a01abc5d0e23873d4c1b8 就是数据库自动为这个Todo生成的Id),这样客户端可以方便的知道指向刚刚生成的Todo的API链接。
执行添加Todo后的返回Json数据
<