Java中实现MongoDB自增主键ID

本文介绍MongoDB中ObjectId的特点及自增ID的手动实现方法,包括定义序列实体类、业务实体类、监听类等内容。

1.了解MongoDB的ObjectId

        MongoDB的文档固定是使用“_id”作为主键的,它可以是任何类型的,默认是个ObjectId对象(在Java中则表现为字符串),那么为什么MongoDB没有采用其他比较常规的做法(比如MySql的自增主键),而是采用了ObjectId的形式来实现?别着急,咱们看看ObjectId的生成方式便可知悉。
        ObjectId使用12字节的存储空间,每个字节两位十六进制数字,是一个24位的字符串。由于看起来很长,不少人会觉得难以处理,其实不然。ObjectId是由客户端生成的,按照如下方式生成:


  • 前4位是一个从标准纪元开始的时间戳,是一个int类别,只不过从十进制转换为了十六进制。这意味着这4个字节隐含了文档的创建时间,将会带来一些有用的属性。并且时间戳处于字符的最前面,同时意味着ObjectId大致会按照插入顺序进行排序,这对于某些方面起到很大作用,如作为索引提高搜索效率等等。使用时间戳还有一个好处是,某些客户端驱动可以通过ObjectId解析出该记录是何时插入的,这也解答了我们平时快速连续创 建多个Objectid时,会发现前几位数字很少发现变化的现实,因为使用的是当前时间,很多用户担心要对服务器进行时间同步,其实这个时间戳的真实值并 不重要,只要其总不停增加就好。
  • 接下来的3个字节,是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectid中间的字符串都是一模一样的原因。
  • 上面的机器字节是为了确保在不同机器产生的ObjectId不冲突,而PID就是为了在同一台机器不同的mongodb进程产生了ObjectId不冲突。
  • 前面的9个字节是保证了一秒内不同机器不同进程生成ObjectId不冲突,最后的3个字节是一个自动增加的计数器,用来确保在同一秒内产生的ObjectId也不会冲突,允许256的3次方等于16777216条记录的唯一性。
        因此,MongoDB不使用自增主键,而是使用ObjectId。在分布式环境中,多个机器同步一个自增ID不但费时且费力,MongoDB从一开始就是设计用来做分布式数据库的,处理多个节点是一个核心要求,而ObjectId在分片环境中要容易生成的多。

2.手动实现自增ID

        ObjectId确实是有很大的好处,但有时候由于某些不可抗力的因素或需求,我们仍需要实现一个自增的数值ID,笔者查阅了网上的资料,大多都是一个套路:使用一个单独的集合A来记录每个集合中的ID最大值,然后每次向集合B中插入文档时,去查找集合A中集合B所对应的ID最大值,取出来并+1,然后更新集合A、根据这个ID再插入文档。下面笔者通过网上一种自认为好点的方式来实现,因为笔者用的是Spring Data MongoDB……


2.1 定义序列实体类SeqInfo

我们需要用这个集合来存储每个集合的ID记录自增到了多少,如下代码:

[java]  view plain  copy
  1. package com.jastar.autokey.entity;  
  2.   
  3. import org.springframework.data.annotation.Id;  
  4. import org.springframework.data.mongodb.core.mapping.Document;  
  5. import org.springframework.data.mongodb.core.mapping.Field;  
  6.   
  7. /** 
  8.  * 模拟序列类 
  9.  *  
  10.  * @author Jastar·Wang 
  11.  * @date 2017年5月27日 
  12.  */  
  13. @Document(collection = "sequence")  
  14. public class SeqInfo {  
  15.   
  16.     @Id  
  17.     private String id;// 主键  
  18.   
  19.     @Field  
  20.     private String collName;// 集合名称  
  21.   
  22.     @Field  
  23.     private Long seqId;// 序列值  
  24.   
  25.     // 省略getter、setter  
  26.   
  27. }  

2.2 定义注解AutoIncKey

我们需要通过这个注解标识主键ID需要自动增长,如下代码:

[java]  view plain  copy
  1. package com.jastar.autokey.annotation;  
  2.   
  3. import java.lang.annotation.ElementType;  
  4. import java.lang.annotation.Retention;  
  5. import java.lang.annotation.RetentionPolicy;  
  6. import java.lang.annotation.Target;  
  7.   
  8. /** 
  9.  * 自定义注解,标识主键字段需要自动增长 
  10.  * <p> 
  11.  * ClassName: AutoIncKey 
  12.  * </p> 
  13.  * <p> 
  14.  * Copyright: (c)2017 Jastar·Wang,All rights reserved. 
  15.  * </p> 
  16.  *  
  17.  * @author jastar-wang 
  18.  * @date 2017年5月27日 
  19.  */  
  20. @Target(ElementType.FIELD)  
  21. @Retention(RetentionPolicy.RUNTIME)  
  22. public @interface AutoIncKey {  
  23.       
  24. }  

2.3 定义业务实体类Student

[java]  view plain  copy
  1. package com.jastar.autokey.entity;  
  2.   
  3. import org.springframework.data.annotation.Id;  
  4. import org.springframework.data.mongodb.core.mapping.Document;  
  5. import org.springframework.data.mongodb.core.mapping.Field;  
  6.   
  7. import com.jastar.autokey.annotation.AutoIncKey;  
  8.   
  9. @Document(collection = "student")  
  10. public class Student {  
  11.   
  12.     @AutoIncKey  
  13.     @Id  
  14.     private Long id = 0L;// 为什么赋了默认值?文章后说明  
  15.   
  16.     @Field  
  17.     private String name;  
  18.   
  19.     // 省略getter、setter  
  20. }  

2.4 定义监听类SaveEventListener

→2017年7月26日更新:

        注意下面代码中重写的onBeforeConvert方法在1.8版本开始就废弃了,不过官方推荐: Please use onBeforeConvert(BeforeConvertEvent),各位猿友可以研究下这个方法如何使用,我想 BeforeConvertEvent 对象里面应该会有所需要的参数信息,在此我就不再亲测了。


因为使用的是Spring Data MongoDB,所以可以重写监听事件里面的方法,而进行某些处理,该类需要继承AbstractMongoEventListener类,并且需交由Spring管理,如下代码:

[java]  view plain  copy
  1. package com.jastar.autokey.listener;  
  2.   
  3. import java.lang.reflect.Field;  
  4.   
  5. import org.springframework.beans.factory.annotation.Autowired;  
  6. import org.springframework.data.mongodb.core.FindAndModifyOptions;  
  7. import org.springframework.data.mongodb.core.MongoTemplate;  
  8. import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;  
  9. import org.springframework.data.mongodb.core.query.Criteria;  
  10. import org.springframework.data.mongodb.core.query.Query;  
  11. import org.springframework.data.mongodb.core.query.Update;  
  12. import org.springframework.stereotype.Component;  
  13. import org.springframework.util.ReflectionUtils;  
  14.   
  15. import com.jastar.autokey.annotation.AutoIncKey;  
  16. import com.jastar.autokey.entity.SeqInfo;  
  17.   
  18. /** 
  19.  * 保存文档监听类<br> 
  20.  * 在保存对象时,通过反射方式为其生成ID 
  21.  * <p> 
  22.  * ClassName: SaveEventListener 
  23.  * </p> 
  24.  * <p> 
  25.  * Copyright: (c)2017 Jastar·Wang,All rights reserved. 
  26.  * </p> 
  27.  *  
  28.  * @author jastar-wang 
  29.  * @date 2017年5月27日 
  30.  */  
  31. @Component  
  32. public class SaveEventListener extends AbstractMongoEventListener<Object> {  
  33.   
  34.     @Autowired  
  35.     private MongoTemplate mongo;  
  36.   
  37.     @Override  
  38.     public void onBeforeConvert(final Object source) {  
  39.         if (source != null) {  
  40.             ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {  
  41.                 public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {  
  42.                     ReflectionUtils.makeAccessible(field);  
  43.                     // 如果字段添加了我们自定义的AutoIncKey注解  
  44.                     if (field.isAnnotationPresent(AutoIncKey.class)) {  
  45.                         // 设置自增ID  
  46.                         field.set(source, getNextId(source.getClass().getSimpleName()));  
  47.                     }  
  48.                 }  
  49.             });  
  50.         }  
  51.     }  
  52.   
  53.     /** 
  54.      * 获取下一个自增ID 
  55.      *  
  56.      * @param collName 
  57.      *            集合(这里用类名,就唯一性来说最好还是存放长类名)名称 
  58.      * @return 序列值 
  59.      */  
  60.     private Long getNextId(String collName) {  
  61.         Query query = new Query(Criteria.where("collName").is(collName));  
  62.         Update update = new Update();  
  63.         update.inc("seqId"1);  
  64.         FindAndModifyOptions options = new FindAndModifyOptions();  
  65.         options.upsert(true);  
  66.         options.returnNew(true);  
  67.         SeqInfo seq = mongo.findAndModify(query, update, options, SeqInfo.class);  
  68.         return seq.getSeqId();  
  69.     }  
  70. }  

2.5 单元测试

[java]  view plain  copy
  1. @Test  
  2. public void save() {  
  3.     Student stu = new Student();  
  4.     stu.setName("张三");  
  5.     service.save(stu);  
  6.     // service.update(stu);  
  7.     System.out.println("已生成ID:" + stu.getId());  
  8. }  

2.6 总结

经过测试,以上流程没有问题,会得到期望的结果,但是有以下几点需要注意:

(1)为什么我在Student类中为主键赋了一个默认值0L?

答:我在此自增方式原作者文章中发现这么一句,“注意自增ID的类型不要定义成Long这种包装类,mongotemplate的源码里面对主键ID的类型有限制”。测试后发现,如果ID定义为原生类型确实是没有问题的。当ID定义为包装类的情况下,如果在onBeforeConvert方法之前没有给ID设置值,是会报错的,我猜测可能是因为内部转换类型时如果ID是空值而无法转换引起的,因此,我赋了一个默认值,这样就不会报错了,包装类也可以使用(不过这样好像跟原生类型就没什么区别了,没什么意义)。

(2)这个监听器会不会影响修改操作?

答:测试发现,不会影响,水平有限,本人也不知作何解释,不要打我……

(3)这种方式会有并发问题吗?

答:不会的!根据官方文档说明,findAndModify一个原子性操作,不过有这么一句“When the findAndModify command includes the upsert: true option and the query field(s) is not uniquely indexed, the command could insert a document multiple times in certain circumstances.”,大概意思是说当查询和更新两个操作都存在时,如果查询的字段没有唯一索引的话,该命令可能会在某些情况下更新/插入 文档多次,参考链接:戳我戳我以上演示的是只存储了集合所对应的实体类的短名称,短名称是会重复的,所以这种方法不妥,还是记录长名称吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值