前言
MongoDB
使用_id
作为文档内部数据主键,正常情况下,我们也默认使用它作为文档内部数据的主键,它默认是个ObjectId
对象。
如下形式:
在Java
中,它表现为字符串类型。
它在MongoDB
中使用12字节的存储空间,每个字节两位十六进制数字,是一个24位的字符串。
ObjectId
的生成方式如下图:
61 | 7b | db | ff | 69 | a0 | 5c | 6d | 95 | 4d | 22 | 35 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
- 前4位是一个从标准纪元开始的时间戳,是一个
int
类别,只不过从十进制转换为了十六进制。这意味着这4个字节隐含了文档的创建时间,将会带来一些有用的属性。并且时间戳处于字符的最前面,同时意味着ObjectId
大致会按照插入顺序进行排序,这对于某些方面起到很大作用,如作为索引提高搜索效率等等。使用时间戳还有一个好处是,某些客户端驱动可以通过ObjectId
解析出该记录是何时插入的,这也解答了我们平时快速连续创 建多个ObjectId
时,会发现前几位数字很少发现变化的现实,因为使用的是当前时间,很多用户担心要对服务器进行时间同步,其实这个时间戳的真实值并 不重要,只要其总不停增加就好。 - 接下来的3个字节,是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器
hash
值,确保在分布式中不造成冲突,这也就是在同一台机器生成的ObjectId
中间的字符串都是一模一样的原因。 - 上面的机器字节是为了确保在不同机器产生的
ObjectId
不冲突,而PID
就是为了在同一台机器不同的MongoDB
进程产生了ObjectId
不冲突。 - 前面的9个字节是保证了一秒内不同机器不同进程生成
ObjectId
不冲突,最后的3个字节是一个自动增加的计数器,用来确保在同一秒内产生的ObjectId
也不会冲突,允许256的3次方等于16777216条记录的唯一性。
由于MongoDB
一开始设计就是用来做分布式数据库的,因此相比于关系型数据库(如Mysql
)里的自增id
,或者一些场景的UUID
,其不仅能保证数据处理速度、唯一性要求,而且通过ObjectId
本身,也能提供一些有用的信息。
主键类型 | 随机性 | 占用空间 | 并发支持 | 自身意义(自身包含的信息) |
---|---|---|---|---|
自增Id | 差 | 低 | 差 | 差 |
UUID | 好 | 高 | 好 | 差 |
ObjectId | 好 | 中 | 好 | 好 |
但是,总有一些特殊情况,我们可能需要在MongoDB
中使用自增id
或者UUID
,这种情况,我们应该如何处理呢?
正文
MongoDB
本身并没有提供自增主键或者UUID
等的生成逻辑,但是我们可以通过程序,为指定的字段赋值。
注:
以下代码基于 springboot+mongodb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我们先来看下MongoDB
自身ObjectId
的处理方式。
比如对于下面的类。
1 |
|
其对应MongoDB
中的user
集合。
_id
表示文档的唯一id
,即ObjectId
,我们为其添加@Id
注解,新增文档时,就会生成一个唯一的文档id
。这是Spring data
通用注解,当然我们也可以直接指定@MongoDB
注解,而且@MongoDB
注解可以自定义Id
的转换形式。
1 |
|
可以看到,对于ObjectId
,我们只需要添加注解即可以生成,无需过多关心。
注:
这儿要注意
FieldType
一旦确定了类型不要随便改哦,否则可能出现下面这样的问题。
这两个文档
_id
其实是不一样的,一个是用FieldType.OBJECT_ID
生成的,一个是用FieldType.STRING
生成的,我们改了FieldType
类型,进行更新的时候,就会出现两个看起来很相似的文档了……
再回到我们正题,假设我们想要一个唯一的userId
(UUID)和一个唯一的longId
(自增id)字段呢?
对于userId
或者longId
,当然我们可以在每次新增文档时,手动去进行设置,大致如下:
1 | User user = new User(); |
这种操作并不方便,并且可能会有遗漏。
其实我们也可以和上面提到的@Id
注解一样,自定义两个注解,一个代表自增主键,一个代表UUID
。
然后通过获取注解并进行一些操作,为两个字段赋值。
我们来看下:
1 |
|
我们定义了两个注解@IncKey
和@UUIDKey
,一个代表自增主键,一个代表UUID
。
要解析它们并且进行处理,需要在一个公共点,即应该在保存文档之前,所有文档的保存都应该过这个点。
spring-data-mongo
中恰好给我们提供了这样一个类,它在常用事件的生命周期内提供一些方法,我们来看下。
AbstractMongoEventListener
就是可以监听常用事件生命周期的一个Abstract
类,其结构如下:
onBeforeConvert
:在将对象转换为Document
之前,在MongoTemplate
中调用insert
、insertList
和save
操作。onBeforeSave
:在数据库中插入或保存Document
之前,在MongoTemplate
中调用insert
、insertList
和save
操作。onAfterSave
:在数据库中插入或保存Document
之后,在MongoTemplate
中调用insert
、insertList
和save
操作。onAfterLoad
:在从数据库检索文档之后,在MongoTemplate
的find
、findAndRemove
、findOne
和getCollection
方法中调用。onAfterConvert
:在从数据库中检索到文档并转换为POJO
之后,在MongoTemplate
的find
、findAndRemove
、findOne
和getCollection
方法中调用。onAfterDelete
:在从数据库中删除一个或一组文档之后,在MongoTemplate
的remove
方法中调用。onBeforeDelete
:在从数据库中删除一个或一组文档之前,在MongoTemplate
的remove
方法中调用。
对于我们上述两个字段的赋值,我们可以在对象被转为Documnet
之前进行设置,也就是重写onBeforeConvert
方法。
方法中我们需要通过反射拿到有我们自定义注解的字段,然后对字段值进行赋值操作。
对于UUID
的生成,还是比较简单的。然而如何生成自增主键呢?
我们先来看下整体代码:
1 |
|
可以看到,对于自增主键,我们使用了getNextId
来获取,需要传入collName
,这是针对于每个集合,自增主键都是从0开始单独计数。
我们创建了一个sequence
集合用来保存其它需要自增主键的集合当前的自增主键id
值。
1 |
|
使用了mongoTemplate
的findAndModify
操作,该操作具有原子性,可以保证并发情况下的数据正确性。
我们写上Dao
层:
1 |
|
然后用测试类进行测试。
1 |
|
1 |
|
运行后可以看到生成的Document
携带 longId
和userId
。
我们把User
上的两个自定义注解去掉,运行就会发现新生成的文档不会携带这两个字段。
由于我们判断的是有自定义注解时,该字段为空才当作新增处理,因此如果我们对文档进行修改或删除(非此两个字段),并不会影响到它们。
1 |
|
最后demo
项目整体结构如下图,上述代码既是该demo
的全部代码。
总结
本文我们学到了如何操作mongoTemplate
生成自定义主键的问题。
其实mongodb
大部分场景不需要我们去手动生成主键,既不方便也不实用。
但通过这篇文章,我们还可以了解到一些方法,如想在文档保存前进行其他一些操作,那么根据上述方法,也能轻松应对处理。