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记录自增到了多少,如下代码:

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
 
}

2.2 定义注解AutoIncKey
我们需要通过这个注解标识主键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;
 
/**
 * 自定义注解,标识主键字段需要自动增长
 * <p>
 * ClassName: AutoIncKey
 * </p>
 * <p>
 * Copyright: (c)2017 Jastar·Wang,All rights reserved.
 * </p>
 *
 * @author jastar-wang
 * @date 2017年5月27日
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIncKey {
    
}
2.3 定义业务实体类Student
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
}

2.4 定义监听类SaveEventListener
→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;
 
/**
 * 保存文档监听类<br>
 * 在保存对象时,通过反射方式为其生成ID
 * <p>
 * ClassName: SaveEventListener
 * </p>
 * <p>
 * Copyright: (c)2017 Jastar·Wang,All rights reserved.
 * </p>
 *
 * @author jastar-wang
 * @date 2017年5月27日
 */
@Component
public class SaveEventListener extends AbstractMongoEventListener<Object> {
 
    @Autowired
    private MongoTemplate mongo;
 
    @Override
    public void onBeforeConvert(BeforeConvertEvent<Object> event) {
        final Object source = event.getSource();
        if (source != null) {
            ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
                /** Perform an operation using the given field.
                 * @param field the field to operate on */
                @Override
                public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                    ReflectionUtils.makeAccessible(field);
                    // 如果字段添加了我们自定义的AutoIncKey注解
                    if (field.isAnnotationPresent(AutoIncKey.class)
                            //判断注解的字段是否为number类型且值是否等于0.如果大于0说明有ID不需要生成ID
                            && field.get(source) instanceof Number
                            && field.getLong(source) == 0) {
                        // 设置自增ID
                        field.set(source, getNextId(source.getClass().getSimpleName()));
                        logger.debug("increase key, source = {} , nextId = {}", source, field.get(source));
                    }
                }
            });
        }
    }
 
    /**
     * 获取下一个自增ID
     *
     * @param collName
     *            集合(这里用类名,就唯一性来说最好还是存放长类名)名称
     * @return 序列值
     */
    private Long getNextId(String collName) {
        Query query = new Query(Criteria.where("collName").is(collName));
        Update update = new Update();
        update.inc("seqId", 1);
        FindAndModifyOptions options = new FindAndModifyOptions();
        options.upsert(true);
        options.returnNew(true);
        SeqInfo seq = mongo.findAndModify(query, update, options, SeqInfo.class);
        return seq.getSeqId();
    }
}

2.5 单元测试
@Test
public void save() {
    Student stu = new Student();
    stu.setName("张三");
    service.save(stu);
    // service.update(stu);
    System.out.println("已生成ID:" + stu.getId());
}
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.”,大概意思是说当查询和更新两个操作都存在时,如果查询的字段没有唯一索引的话,该命令可能会在某些情况下更新/插入 文档多次,参考链接:戳我戳我。以上演示的是只存储了集合所对应的实体类的短名称,短名称是会重复的,所以这种方法不妥,还是记录长名称吧。

Java中实现MongoDB自增主键ID的更多相关文章

  1. Mybatis 中获取添加的自增主键ID(针对mysql)

    分享一篇博客,主要就是针对在我们使用SSM的时候,在.xml中获取<insert></insert> 时的自增主键Id,由于好久没有,这个时候使用,有点生疏,就在这里写个笔记, ...

  2. mybatis插入数据后返回自增主键ID详解

    1.场景介绍: ​ 开发过程中我们经常性的会用到许多的中间表,用于数据之间的对应和关联.这个时候我们关联最多的就是ID,我们在一张表中插入数据后级联增加到关联表中.我们熟知的mybatis在插入数据后 ...

  3. Mysql对自增主键ID进行重新排序

    Mysql数据库表的自增主键ID号经过一段时间的添加与删除之后乱了,需要重新排列. 原理:删除原有的自增ID,重新建立新的自增ID. 1,删除原有主键: ALTER TABLE `table_name ...

  4. Mysql自增主键ID重新排序方法详解

    Mysql数据库表的自增主键ID号乱了,需要重新排列. 原理:删除原有的自增ID,重新建立新的自增ID. 1,删除原有主键: ALTER TABLE `table_name` DROP `id`; 2 ...

  5. mybaits返回自增主键ID

    mybaits两种获取自增主键ID的方法:一种是使用useGeneratedKeys,第二种是selectKey方法获取. useGeneratedKeys <insert id="i ...

  6. Mysql数据库表的自增主键ID号乱了,需要重新排列。

    Mysql数据库表的自增主键ID号乱了,需要重新排列. 原理:删除原有的自增ID,重新建立新的自增ID. 1,删除原有主键:ALTER TABLE `table_name` DROP `id`; 2, ...

  7. mybatis插入操作时,返回自增主键id

    mapper.xml 代码 <insert id="insert" parameterType="com.Student" > <select ...

  8. 使用mybatis插入自增主键ID的数据后返回自增的ID

    在开发中碰到用户注册的功能需要用到用户ID,但是用户ID是数据库自增生成的,这种情况上网查询后使用下面的方式配置mybatis的insert语句可以解决: <insert id="in ...

  9. Mybatis+Mysql插入数据库返回自增主键id值的三种方法

    一.场景: 插入数据库的值需要立即得到返回的主键id进行下一步程序操作 二.解决方法: 第一种:使用通用mapper的插入方法 Mapper.insertSelective(record): 此方法: ...

随机推荐

  1. 关于C++ 中的this 的理解

    关键字this 通常被用在一个class内部,指正在被执行的该class的对象(object)在内存中的地址.它是一个指针,其值永远是自身object的地址.

  2. Git使用http clone客户端保存用户名密码

    使用Git Bash时,用命令git pull或git push时总是要输入密码,很烦躁 解决办法 需要注意的是,这个方法是在Windows下使用 1. 新建环境变量   HOME 值为 %USERP ...

  3. 【bzoj2815】灾难[ZJOI2012](拓扑排序+lca)

    题目传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=2815 原版题解:http://fanhq666.blog.163.com/blog/st ...

  4. [转载]ORA-00313:无法打开日志组1(线程 1)的成员_ORA-00312:

    原文地址:1)的成员_ORA-00312:">ORA-00313:无法打开日志组1(线程 1)的成员_ORA-00312:作者:Sweet_薇薇毅 今天用系统清理工具把系统垃圾清理了一 ...

  5. ANT+JMETER集成1(生成报告)

    配置build.xml文件时,网上找了各种版本的代码都会报错, 终于找到个可以生成报告的build源码了 链接: http://www.cnblogs.com/hanxiaomin/p/6731810 ...

  6. Python staticmethod

    1 @staticmethod 静态方法 when this method is called, we don't pass an instance of the class to it (as we ...

  7. webdriver 选择

    You only need   from selenium import webdriver . Execute   html= browser.find_element_by_xpath(" ...

  8. Python学习之路day3-函数

    一.函数基础 编程方法典型的编程方法有面向过程.面向对象和函数式编程.面向过程是把编程的重点放在实现过程上,分析出结局问题所需的步骤过程,然后通过语句来一一定义实现.面向对象是把构成问题的事务分界成若 ...

  9. 数据库Dao类BaseDao(增删改)

    package com.changim.patient.app.db; import android.content.ContentValues; import android.content.Con ...

  10. hdu1085 Holding Bin-Laden Captive!(母函数)

    简单的母函数应用. #include<iostream> #include<cstdio> #include<cstdlib> #include<cstrin ...