Springboot MongoDB CRUD(一)

概述

MongoDB 描述等可见其他文章, 本文章主要基于 springboot 项目演示

版本信息:

  • Java JDK 版本:1.8

  • SpringBoot 版本:2.4.5.RELEASE

  • MongoDB 版本:community-4.4

参考地址:

示例项目地址:

项目实战

SpringBoot 操作主要有两种方式,一种基于 JPA(Repositories)、一种基于MongoDB 官方 Java 驱动 MongoTemplate,直接用代码说话吧。

项目构建等忽略, gradle 构建,配置文件:

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.xjxxxc.mongodb.demo'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenLocal()
    maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    implementation 'com.google.guava:guava:30.1.1-jre'
    implementation 'org.apache.commons:commons-lang3'
    implementation 'com.alibaba:fastjson:1.2.76'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'

    testAnnotationProcessor 'org.projectlombok:lombok:1.18.20'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'org.projectlombok:lombok'
}

test {
    useJUnitPlatform()
}

包说明:

├── com
│   └── xjxxxc
│       └── mongodb
│           └── demo
│               ├── MongodbDemoApplication.java # 启动类
│               ├── config # 配置类,如数据源
│               ├── listener # 监听器,如 MongoEvent
│               ├── model # 模型
│               │   └── entity # 实体,JPA 使用
│               ├── repository # JPA repository
│               ├── service # *服务层,提供 MongoDB 特性。
│               └── template # MongoDB 模板类使用 DEMO

项目基本配置

application.properties

server.port=8866
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=test
spring.data.mongodb.username=test
spring.data.mongodb.password=123456

根据情况修改即可,为了演示方便直接写在 application.properties

MongoDBDataSourceConfig

继承 AbstractMongoClientConfiguration 类,重写 mongoClient() 、getDatabaseName() 、mappingMongoConverter() 方法,并且去除 _class 字段。

@Configuration
@EnableMongoRepositories(basePackages = {"com.xjxxxc.mongodb.demo.*"})
public class MongoDBDataSourceConfig extends AbstractMongoClientConfiguration {
    @Value(value = "${spring.data.mongodb.host:127.0.0.1}")
    private String host;

    @Value(value = "${spring.data.mongodb.port:27017}")
    private Integer port;

    @Value(value = "${spring.data.mongodb.database:test}")
    private String database;

    @Value(value = "${spring.data.mongodb.username:test}")
    private String username;

    @Value(value = "${spring.data.mongodb.password:123456}")
    private String password;

    @Override
    public MongoClient mongoClient() {
        MongoClient mongoClient = MongoClients.create(
                MongoClientSettings.builder()
                        // 集群设置
                        .applyToClusterSettings(builder ->
                                builder.hosts(Arrays.asList(new ServerAddress(host, port))))
                        // 凭据
                        .credential(
                                MongoCredential
                                        .createCredential(username, database, password.toCharArray()))
                        .build());
        return mongoClient;
    }


    /**
     * Return the name of the database to connect to.
     *
     * @return must not be {@literal null}.
     */
    @Override
    protected String getDatabaseName() {
        return database;
    }

    /**
     * 去除 _class
     * @param databaseFactory
     * @param customConversions
     * @param mappingContext
     * @return MappingMongoConverter
     */
    @Override
    public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, MongoCustomConversions customConversions, MongoMappingContext mappingContext) {

        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext);
        converter.setCustomConversions(customConversions());
        converter.setCodecRegistryProvider(mongoDbFactory());
        // Don't save _class to mongo
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));

        return converter;
    }


}

Repositories

关于使用简单的 Repositories 方式来操作 MongoDB 这种用法只能实现较简单的操作[可使用Example封装复杂的查询],使用简单但是灵活性比较差。

@CustomerId [自定义 ID 生成策略]

MongoDB 默认会为 _id 生成 ObjectId 对象,但业务使用时可能有自己的生成策略[需配合监听器实现]。

/**
 * 自定义 ID
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomerId {
}

SaveEventListener

继承 AbstractMongoEventListener 重写相应的方法,具体可见文档
备注:onBeforeConvert、onBeforeSave 均可实现功能,具体可见生命周期。


/**
 * MongoDB 监听器
 */
@Slf4j
@Component
public class SaveEventListener extends AbstractMongoEventListener {

    /**
     * 在将Document插入或保存在数据库中之前,在 insert,insertList和save操作中调用。
     * @param event
     */
    @Override
    public void onBeforeSave(BeforeSaveEvent event) {
        super.onBeforeSave(event);
        Object source = event.getSource();
        ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
            @Override
            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                ReflectionUtils.makeAccessible(field);
                if (!field.isAnnotationPresent(CustomerId.class)) {
                    return;
                }
                // 如果字段添加了我们自定义的 @CustomerId 注解
                Object object = field.get(source);
                // 需根据是否有值进行主键生成,save 操作时存在 update 与 insert ,会导致更新时变成 insert 。
                if(Objects.nonNull(object) && !StringUtils.isNotEmpty(object.toString())){
                    try {
                        // 剔除 Id >= OL 的情况 不注入
                        Long tmp = Long.parseLong(object.toString());
                        if(tmp.longValue() > 0L){
                            return;
                        }
                    }catch (Exception e){
                        // 非法 ID
                        log.info("自动注入 ID 有异常,{}", e.getMessage());
                    }
                }
                // 具体业务使用其他方式
                field.set(source, RandomUtils.nextLong());
            }
        });

        if(log.isDebugEnabled()){
            log.debug("onBeforeSave -> {}", event);
        }
    }

    /**
     * 在 object 被MongoConverter转换为Document之前,在MongoTemplate insert,insertList和save操作中调用。
     * @param event
     */
    @Override
    public void onBeforeConvert(BeforeConvertEvent event) {
        Object source = event.getSource();
        ReflectionUtils.doWithFields(source.getClass(), new ReflectionUtils.FieldCallback() {
            @Override
            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                ReflectionUtils.makeAccessible(field);
                if (!field.isAnnotationPresent(CustomerId.class)) {
                    return;
                }
                // 如果字段添加了我们自定义的 @CustomerId 注解
                Object object = field.get(source);
                // 需根据是否有值进行主键生成,save 操作时存在 update 与 insert ,会导致更新时变成 insert 。
                if(Objects.nonNull(object) && !StringUtils.isNotEmpty(object.toString())){
                    try {
                        // 剔除 Id >= OL 的情况 不注入
                        Long tmp = Long.parseLong(object.toString());
                        if(tmp.longValue() > 0L){
                            return;
                        }
                    }catch (Exception e){
                        // 非法 ID
                        log.info("自动注入 ID 有异常,{}", e.getMessage());
                    }
                }
                // 具体业务使用其他方式
                field.set(source, RandomUtils.nextLong());
            }
        });
        if(log.isDebugEnabled()){
            log.debug("onBeforeConvert -> {}", event);
        }
    }
}

BaseEntity

实际项目中,都会存在部分通用字段,需抽出父类,故示例也尽量结合项目。

/**
 * 基础 Entity ,抽出公用字段
 */
@Getter
@Setter
@SuperBuilder
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity implements Serializable {

    /**
     * 使用自定义 ID
     */
    @Id
    @CustomerId
    private Long id;
    
    @Field("saas_id")
    @Indexed
    protected Long saasId;

    @Field("del_status")
    protected Integer delStatus;

    @CreatedBy
    @Field("create_user")
    protected String createUser;

    @LastModifiedBy
    @Field("update_user")
    protected String updateUser;

    @CreatedDate
    @Field("create_time")
    protected Date createTime;

    @LastModifiedDate
    @Field("update_time")
    protected Date updateTime;

    /**
     * 创建基础字段默认值
     */
    public void initBaseProperties() {
        if (this.delStatus == null) {
            this.delStatus = 1;
        }
        if (createUser == null) {
            this.createUser = "SYSTEM";
        }
        if (updateUser == null) {
            this.updateUser = "SYSTEM";
        }
        if (createTime == null) {
            this.createTime = new Date(System.currentTimeMillis());
        }
        if (updateTime == null) {
            this.updateTime = new Date(System.currentTimeMillis());
        }
    }
}

Post

实现业务对象 Post, 继承 BaseEntity

/**
 * Post entity
 */
@Data
@SuperBuilder
@Document(collection = "post")
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper=false)
public class Post extends BaseEntity {
    private String title;

    private String content;

    private String remark;

    private List<Integer> testList;

}

PostRepository

继承 MongoRepository

/**
 * Post Repository
 */
@Repository
public interface PostRepository extends MongoRepository<Post, String> {
}

PostRepositoryTest

测试 PostRepository 单元测试。

/**
 * PostRepository 测试
 */
@SpringBootTest
class PostRepositoryTest {

    @Autowired
    private PostRepository postRepository;

    /**
     * 测试 Post 实例
     */
    Post post = Post.builder()
            .content("conten of " + Math.random() * 10000)
            .title("conten of " + Math.random() * 10000)
            .remark("")
            .build();

    /**
     * 测试 Repository insert
     */
    void insert(){
        Post insertPost = postRepository.insert(post);
        Assert.notNull(insertPost,"PostRepository insert -> error");
    }


    /**
     * 测试 Repository save
     */
    void save(){
        // save 方法会根据主键来确定是 insert or update
        Post insertPost = postRepository.save(post);
        Assert.notNull(insertPost,"PostRepository save -> insert error");

        insertPost.setRemark("update");
        Post updatePost = postRepository.save(insertPost);
        Assert.isTrue("update".equals(updatePost.getRemark()),"PostRepository save -> update error");

    }

    /**
     * 测试 Repository findAll
     */
    void findAll(){
        List<Post> postList = postRepository.findAll();

        Assert.notEmpty(postList,"PostRepository findAll -> error");
    }

    /**
     * 测试 Repository findAll
     */
    void findById(){
        Post insertPost = postRepository.save(post);
        Assert.notNull(insertPost,"PostRepository findAll -> insert error");

        Optional<Post> queryPost = postRepository.findById(String.valueOf(insertPost.getId()));
        Assert.isTrue(queryPost.isPresent(),"PostRepository findById -> error");
    }

    /**
     * 测试 Repository findAll
     */
    void findOne(){
        post.setRemark("findOne");
        Post insertPost = postRepository.save(post);
        Assert.notNull(insertPost,"PostRepository findAll -> insert error");

        Post queryCondition = Post.builder().remark("findOne").build();
        Example<Post> postExample= Example.of(queryCondition);
        Optional<Post> queryPost = postRepository.findOne(postExample);
        Assert.isTrue(queryPost.isPresent(),"PostRepository findOne -> error");
    }

    /**
     * 测试 Repository delete
     */
    void delete(){
        // 先插入数据
        post.setRemark("delete");
        Post insertPost = postRepository.save(post);

        postRepository.delete(insertPost);

        Optional<Post> queryPost = postRepository.findById(String.valueOf(insertPost.getId()));
        Assert.isTrue(!queryPost.isPresent(),"PostRepository delete -> error");
    }

}

以上就简单的把使用 Repository 实现的例子演示了下。代码待整个例子实现一起上传~

MongoTemplate

这里使用 Spring Data MongoDB 封装的 MongoDB 官方 Java 驱动 MongoTemplate 对 MongoDB 进行操作。

下偏文章将继续~

备注:个人博客同步至简书。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容