在用spring-data-mongodb向mongodb里写数据的时候,默认主键是一个很长的字符串,由时间戳,机器码,进程号等等拼凑起来的,保证唯一性,作为分布式数据库也少了一些麻烦,但是如果需要通过mongodb兼容mysql里面的数据,做一些数据迁移操作,对于主键存在关联表字段里,如果不修改数据库表字段类型,那么只有mongodb里保存Long型主键来达到目的
通过mongoTemplate进行insert操作时,无法直接insert自定义的Long型主键,原因是下面这段代码
protected <T> void doInsert(String collectionName, T objectToSave, MongoWriter<T> writer) {
initializeVersionProperty(objectToSave);
maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave, collectionName));
assertUpdateableIdIfNotSet(objectToSave);
DBObject dbDoc = toDbObject(objectToSave, writer);
maybeEmitEvent(new BeforeSaveEvent<T>(objectToSave, dbDoc, collectionName));
Object id = insertDBObject(collectionName, dbDoc, objectToSave.getClass());
populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<T>(objectToSave, dbDoc, collectionName));
}
的assertUpdateableIdIfNotSet方法做了限制,我的spring-data-mongodb版本比较老,1.x.x
想要突破这种限制,可以参考前面一篇《Spring Data MongoDB生命周期时间》中有关onBeforeConvert这个回调方法,在 object 被MongoConverter转换为Document之前,会被调用,因此可以在此刻来创建Long型主键
下面小例子在本地起了一个mongodb,来验证这种解决方案
首先配置mongoTemplate这个bean
package com.lihuia.demo.conf;
import com.mongodb.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
@Configuration
@EnableMongoRepositories(basePackages = "com.lihuia.demo.entity")
public class MongoConfig {
@Bean
public MongoClient mongoClient() {
return new MongoClient("localhost");
}
@Bean
public MongoTemplate mongoTemplate() {
return new MongoTemplate(mongoClient(), "lihuia");
}
}
创建一个collection,目的是保存递增Long型主键的值,同时还要记录collectionName为DO结构体的类名,因为这个collection可以保存多个entity的collection自增主键,seqId就是保存的自增主键的值,要和entity里定义的id类型一致
package com.lihuia.demo.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
@Document(collection = "sequence")
@Data
public class SeqInfo {
@Id
private String id;
@Field
private String collectionName;
@Field
private Long seqId;
}
接着自定义一个注解,表示自增
package com.lihuia.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIncKey {
}
然后将自增注解放在我们需要自增的主键字段上,同时@Id也是mongodb唯一认准的主键字段,就算变量名不是id
package com.lihuia.demo.entity;
import com.lihuia.demo.annotation.AutoIncKey;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
@Document(collection = "user_center")
@Data
public class User extends MongoBaseDO<Long> {
@AutoIncKey
@Id
private Long id;
@Field
private String name;
@Field
private Integer age;
@Field
private String phone;
}
最后就是关键部分,注册一个子类,覆盖onBeforeConvert方法,object转换为document时拦截,获取自定义主键后再写入
package com.lihuia.demo.util;
import com.lihuia.demo.annotation.AutoIncKey;
import com.lihuia.demo.entity.SeqInfo;
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.mapping.event.BeforeConvertEvent;
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 javax.annotation.Resource;
import java.lang.reflect.Field;
@Component
public class InsertEventListener extends AbstractMongoEventListener<Object> {
@Resource
private MongoTemplate mongoTemplate;
@Override
public void onBeforeConvert(BeforeConvertEvent<Object> event) {
Object source = event.getSource();
if (source != null) {
ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
ReflectionUtils.makeAccessible(field);
/** 查询添加了自增注解的字段 */
if (field.isAnnotationPresent(AutoIncKey.class)) {
/** 更新该字段 */
field.set(source, getNextId(source.getClass().getSimpleName()));
}
}
});
}
}
private Long getNextId(String collectionName) {
Query query = new Query(Criteria.where("collectionName").is(collectionName));
Update update = new Update();
/** 主键递增1,设置为Long型 */
update.inc("seqId", 1L);
FindAndModifyOptions options = new FindAndModifyOptions();
options.upsert(true);
options.returnNew(true);
/** 原子操作,修改并返回 */
SeqInfo seqInfo = mongoTemplate.findAndModify(query, update, options, SeqInfo.class);
return seqInfo.getSeqId();
}
}
下面就是测试了,写一个简单的controller进行调用
package com.lihuia.demo.controller;
import com.alibaba.fastjson.JSONObject;
import com.lihuia.demo.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
public class UserController {
@Resource
private MongoTemplate mongoTemplate;
@RequestMapping(value = "/insert", method = RequestMethod.POST)
public Object insertUser(@RequestBody JSONObject params) {
User user = JSONObject.toJavaObject(params, User.class);
mongoTemplate.insert(user);
return "_id=" + user.getId();
}
}
连续调用
可以看到mongodb里保存的,user_center这个collection里,每个document主键是递增Long型
> db.getCollection("user_center").find() { "_id" : NumberLong(1), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(2), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(3), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(4), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(5), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(6), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(7), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(8), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(9), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" } { "_id" : NumberLong(10), "_class" : "com.lihuia.demo.entity.User", "name" : "lihui", "age" : 18, "phone" : "17777777777" }
同时,当前主键的值,保存在sequence这个collection里
> db.getCollection("sequence").find() { "_id" : ObjectId("5f0d7535789fe9522f71d172"), "collectionName" : "User", "seqId" : NumberLong(10) }
所以流程就是,sequence保存不同collection对的当前主键值,然后在数据写入mongodb前,获取next值写入,这里是原子操作,因此不用担心冲突