MongoDB的文档固定是使用“_id”作为主键的,它可以是任何类型的,默认是个ObjectId对象(在Java中则表现为字符串),那么为什么MongoDB没有采用其他比较常规的做法(比如MySql的自增主键),而是采用了ObjectId的形式来实现?别着急,咱们看看ObjectId的生成方式便可知悉。
ObjectId使用12字节的存储空间,每个字节两位十六进制数字,是一个24位的字符串。由于看起来很长,不少人会觉得难以处理,其实不然。ObjectId是由客户端生成的,按照如下方式生成:
ObjectId确实是有很大的好处,但有时候由于某些不可抗力的因素或需求,我们仍需要实现一个自增的数值ID,笔者查阅了网上的资料,大多都是一个套路:使用一个单独的集合A来记录每个集合中的ID最大值,然后每次向集合B中插入文档时,去查找集合A中集合B所对应的ID最大值,取出来并+1,然后更新集合A、根据这个ID再插入文档。下面笔者通过网上一种自认为好点的方式来实现,因为笔者用的是Spring Data MongoDB……
我们需要用这个集合来存储每个集合的ID记录自增到了多少,如下代码:
package com.jastar.autokey.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
/**
* 模拟序列类
*
* @author Jastar·Wang
* @date 2017年5月27日
*/
@Document(collection = "sequence")
public class SeqInfo {
@Id
private String id;// 主键
@Field
private String collName;// 集合名称
@Field
private Long seqId;// 序列值
// 省略getter、setter
}
我们需要通过这个注解标识主键ID需要自动增长,如下代码:
package com.jastar.autokey.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,标识主键字段需要自动增长
*
* ClassName: AutoIncKey
*
*
* Copyright: (c)2017 Jastar·Wang,All rights reserved.
*
*
* @author jastar-wang
* @date 2017年5月27日
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIncKey {
}
package com.jastar.autokey.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import com.jastar.autokey.annotation.AutoIncKey;
@Document(collection = "student")
public class Student {
@AutoIncKey
@Id
private Long id = 0L;// 为什么赋了默认值?文章后说明
@Field
private String name;
// 省略getter、setter
}
→2017年7月26日更新:
注意下面代码中重写的onBeforeConvert方法在1.8版本开始就废弃了,不过官方推荐: Please use onBeforeConvert(BeforeConvertEvent),各位猿友可以研究下这个方法如何使用,我想 BeforeConvertEvent 对象里面应该会有所需要的参数信息,在此我就不再亲测了。
因为使用的是Spring Data MongoDB,所以可以重写监听事件里面的方法,而进行某些处理,该类需要继承AbstractMongoEventListener类,并且需交由Spring管理,如下代码:
package com.jastar.autokey.listener;
import java.lang.reflect.Field;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import com.jastar.autokey.annotation.AutoIncKey;
import com.jastar.autokey.entity.SeqInfo;
/**
* 保存文档监听类
* 在保存对象时,通过反射方式为其生成ID
*
* ClassName: SaveEventListener
*
*
* Copyright: (c)2017 Jastar·Wang,All rights reserved.
*
*
* @author jastar-wang
* @date 2017年5月27日
*/
@Component
public class SaveEventListener extends AbstractMongoEventListener
@Test
public void save() {
Student stu = new Student();
stu.setName("张三");
service.save(stu);
// service.update(stu);
System.out.println("已生成ID:" + stu.getId());
}
经过测试,以上流程没有问题,会得到期望的结果,但是有以下几点需要注意:
(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.”,大概意思是说当查询和更新两个操作都存在时,如果查询的字段没有唯一索引的话,该命令可能会在某些情况下更新/插入 文档多次,参考链接:戳我戳我。以上演示的是只存储了集合所对应的实体类的短名称,短名称是会重复的,所以这种方法不妥,还是记录长名称吧。
好了,文章就到这,就剩我几几了,都下班了,我也要“粽”情端午去咯~
参考文章:http://www.jianshu.com/p/3418e32ce757