MongoDB的基本CRUD操作

1 MongoDB中的文档设计

以MongoDB做数据库进行CRUD操作,先要思考怎么进行MongoDB的文档设计

在进行文档设计之前,先回顾数据库的三范式

1.1 回顾数据库三范式

简单总结来说,数据库三范式如下:

  • 在数据库中,每个表的一个列中不能存在多个值
  • 每个表都必须要拥有一个且只能拥有一个唯一主键
  • 一个表的外键只能关联其他表的主键

1.2 打破第三范式

现有如下需求:

员工表 : id , name , dept_id

部门表 : id , dept_name

要求 查询获取到员工信息与部门信息

按照之前MySQL的经验,表设计和SQL设计如下:

class Employee{
    Long id;
    String name;
    Long dept_id;
}
class Department{
    Long id;
    String dept_name;
}
select e.*,d.* from employee e left join department d where e.dept_id = d.id

但是这样设计有一个缺点,**那就是当数据量非常庞大之后,其多表查询产生的 笛卡尔积 也会非常庞大 , 那应该如何简化呢? **

如果我们将员工表这么设计:

class Employee{
	Long id;
    String name;
    Long dept_id;
    String dept_name;
}

这样不就可以由多表查询–>单标查询,避免产生笛卡尔积 , 直接查询这个员工的部门信息了?

因为这样的设计违反了数据库的第三范式 , 因此我们将这个操作称之为 打破第三范式

其中, 添加的 dept_name 称之为 冗余字段

当然这个操作也存在缺点 : 添加与更改操作,需要维护冗余字段

因此,其适用场景为 : 当需要进行的 DQL 操作量远远大于 DML 操作时

MongoDB推荐使用冗余字段的方式,减少表之间的管理

1.3 MongoDB文档设计

1.3.1 一对一

现在有如下类:

husband : 丈夫

wife : 妻子

显然,丈夫和妻子之间是一对一的关系

MySQL中的表设计如下:

husband:|wife:
idname|idname
1Jack|1Rose
2Trump|2Bitch

两个表之间,采用相同的主键设计

相对应的类设计如下:

class husband{
    Long id;
    String name;
    Wife wife;
}
class wife{
    Long id;
    String name;
}

在MongoDB中,对应一对一的方式,一般使用 内嵌的方式 , 采用的设计如下

husband:
{
	id:xx ,
	name:xx ,
	wife:{
		id:xx ,
		name:xx
	}
}

但是这种设计,当需要将 内嵌字段 拉出来进行独立需求时,会显得非常复杂,于是当遇到这种需求,也只能像MySQL一样了

1.3.2 一对多/多对一

现在有如下类:

employee : 员工

department : 部门

显然员工和部门之间是多对一的关系

MySQL中的表设计如下:

employee:|department:
idnamedept_id|idname
1Jack2|1Sell
2Trump1|2Fire

相对应的类设计如下:

class employee{
    Long id;
    String name;
    Department dept;
}
class department{
    Long id;
    String name;
}

在MongoDB中,还是推荐使用内嵌的方式 :

Employee:
{
	id:xx ,
	name:xx ,
	department:{
		id:xx ,
		name:xx
	}
}

缺陷和一对一时是一样的

需要注意的是, 一对一关系和一对多关系,在 MongoDB 中,也都支持使用 冗余字段 的写法进行

1.3.3 多对多

现在有如下类 :

student : 学生

teacher : 老师

显然他们之间是 多对多 关系

在MySQL中,会采用中间表来关联他们的关系 :

student_idteacher_id
11
12
21

对应类设计如下 :

class student{
    Long id;
    String name;
    List<Teacher> teachers;    // 此处推荐使用集合而不是数组
}
class teacher{
    ...
}

在MongoDB中,会选择使用数组来保存这种 多对多 关系 (也可以用来保存 一对多 关系)

当teacher的数量不多时:

student{
	id:xx,
	name:xx,
	teacher:[
        {id,name},
        {id,name},
        ...
    ]
}

但是,这种设计有一个缺点,当teacher的字段和数量很多时,这种书写不仅麻烦,而且有一个问题

我们知道在MongoDB中,一个文档的大小不能超过 16M , 当 teacher 数量庞大时,我们可以使用这种书写方式 :

student{
	id:xx,
	name:xx,
	teacher:[
        "teacherId1",
        "teacherId2",
        ...
    ]
}

2 SpringBoot集成MongoDB

  1. 新建 Maven 项目

  2. pom.xml 中指定父依赖,并导入 MongoDB 自动配置依赖

  3. 新建 application.properties 配置文件并配置数据库连接

  4. 创建对应实体类并在实体类上贴上 @Document(" 集合名 ") 注解

  5. 创建 repository 包并在里面创建 实体类Repository 接口 , 使该接口继承 MongoRepository 接口,该接口泛型 第一个为操作的对象,第二个为操作对象主键类型

  6. 创建SpringBoot主配置类并启动SpringBoot

  7. 书写业务层接口及其实现类

3 继承后基本CURD操作及其原理

书写业务层接口及其实现类如下:

public interface IUserService {

    void save(User user);

    void update(User user);

    void delete(String id);

    User get(String id);

    List<User> list();
}
@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserRepository repository;

    @Override
    public void save(User user) {
        repository.save(user);
    }

    @Override
    public void update(User user) {
        repository.save(user);
    }

    @Override
    public void delete(String id) {
        repository.deleteById(id);
    }

    @Override
    public User get(String id) {
        Optional<User> op = repository.findById(id);
        return op.orElse(null);
    }

    @Override
    public List<User> list() {
        return repository.findAll();
    }
}

这里有几个问题和细节:

  • save和update,调用的方法都是 repository.save() , 区别就在于参数中有没有带 id , 带了就是改 , 没带就是增

  • 新增操作时,会发现数据库表格中新增了一列 : _class :
    在这里插入图片描述

​ 其原因是 : 插入数据时,会给所有 属性 增加列,因为其他属性都有对应列了,所以没有新增

​ 但是我们知道,所有类都继承于 Object 类,该类有一个方法 getClass() , 根据属性的定义 , 于是 所有类都有一个隐藏的属性 class , 所以便新增了一个 _class 列

  • 新增操作时,如果这么书写

    @Test
    public void testUpdate(){
        User user = new User();
        user.setId("5f2563b01e350000400009bb");
        user.setName("Potter");
        userService.update(user);
    }
    

    那么会产生这种结果:

在这里插入图片描述

两列数据消失

这是因为 spring-data-mongodb 里面的更新操作,称之为全量更新,一旦执行该操作,所有字段都会被更新

其实其底层就是拿新数据覆盖了原来的数据,所以一旦某些字段我们没有为其赋值,更新后他们就会变为 N/A

想要正常更新,就需要 先查询到对应数据,再将新数据set进去,再利用这个对象进行更新:

// 先获取想要更新的对象
User user = userService.get("5f2563b11e350000400009bd");
// 将我们想要更新的值set进去
user.setName("法海");
// 再更新
userService.update(user);
  • 现在来到最重要的一个点 : 为什么 UserRepository 接口不书写任何方法,只是继承了 MongoRepository 接口, 业务层就可以直接调用 CRUD 方法?

    查看 UserRepository 接口继承体系:

在这里插入图片描述

在上面那些接口中定义了 CRUD 方法,所以 UserRepository 也继承了对应 CRUD 方法

  • 另外 , UserRepository 接口在表面上没有创建对象,也可以调用方法,其实是因为 spring 容器在底层通过 JDK 动态代理的方式,帮我们创建了实现类,通过 .getClass() 方法可以看出来:

在这里插入图片描述

SpringBoot会自动将继承了 MongoRepository 接口 的接口,进行动态代理 ,创建这个接口的代理实现对象

4 JPA查询规范

当我们想通过姓名等查询用户信息怎么办?

我们只需要在 UserRepository 接口中,创建对应的符合 JPA 查询规范的方法就可以

JPA查询规范:

​ 定义符合JPA约束的查询方法,其格式如下:

​ 前缀 + 属性名 + 操作符

​ 其中,前缀需要以 findBy / queryBy 开头

​ 属性名为 对应表的一个列名,并将首字母大写

​ 操作符是类似于 and 和 or 等

其原理是,spring在动态代理时,根据JPA查询规范,解析我们定义的方法,拼接出对应的MQL语句

JPA规范如下 :

在这里插入图片描述

5 MongoTemplate

使用JPA查询,简单也方便,但是当查询条件增多导致JPA无法进行拼接时,此时就需要使用到 :

MongoTemplate 模板方式

这是 spring-data-mongodb 的第二种查询方式,他是 spring-data 规范下创建的子项目,其核心类为 MongoTemplate

其中,还有一些常用的其他类:

  • Query ( org.springframework.data.mongodb.core.query ) : 表示查询对象
  • Criteria : 用来封装查询条件,其静态方法和非静态方法都有相关查询条件的方法
  • Pageable : 用来封装分页查询结果的类

使用 MongoTemplate 的主要流程为 :

  1. 注入 MongoTemplate 类
  2. 创建 查询对象类Query 的对象
  3. 根据需求决定是否创建 Criteria 类的对象 ,不创建就使用其静态方法封装查询条件,创建可以使用其非静态方法封装查询条件,非静态方法中主要涉及到 运算符 等方法
  4. 将查询条件封装到查询对象中 : query.addCriteria(criteria)
  5. 使用 MongoTemplate 的对象调用 find() 查询方法,传入 查询对象,返回值类型,集合名(这个可以省略)

案例:

// 查询 name=法海 的用户
@Test
public void testQuery2(){
	// 查询对象
	Query query = new Query();
	// 用来封装条件
	Criteria criteria = Criteria.where("name").is("法海");
	// 将条件封装到查询对象中
	query.addCriteria(criteria);
	// 执行DQL语句
	// List<User> users = mt.find(query, User.class,"users");
	// 第三个参数 users 实际上可以省略,因为对应实体类上贴了 @Document 注解指定了集合类型
	List<User> users = mt.find(query, User.class);
	System.err.println(users);
}


// 分页查询,显示第二页,每页显示4条信息,并按照id升序排序
@Test
public void testQuery3(){
     Query query = new Query();
     /*
     // 指定排序规则 : 以 id 进行排序,升序排序, Direction是一个枚举类
     Sort sort = Sort.by(Sort.Direction.ASC,"id");
     // 将排序规则加到查询对象中
     query.with(sort);
     // 指定分页条件并加到查询对象中
     query.skip(4);
     query.limit(4);
     */
     // 上面的操作可以替换如下:
     /**
      * 此时的参数
      * 第一个: 相当于 currentPage
      * 第二个: 相当于 pageSize
      * 第三个: 排序规则
      * 第四个: 排序对象
      */
     Pageable pageable = PageRequest.of(1, 4, Sort.Direction.ASC, "id");
     query.with(pageable);
     List<User> users = mt.find(query, User.class);
     users.forEach(System.err::println);
}


// 查询所有 name=法海 或者 age<28 的用户
@Test
public void testQuery4(){
	Query query = new Query();
	Criteria criteria = new Criteria();
	// 此时就需要用到 or 或者运算符了
	// 使用 or 运算符,在里面拼接查询条件
	criteria.orOperator(
	        Criteria.where("name").is("法海"),
	        Criteria.where("age").lt(28)
	);
	// 设置完条件再放进来
	query.addCriteria(criteria);
	List<User> users = mt.find(query, User.class);
	users.forEach(System.err::println);
}

// 查询 name中含有 da 并且 25<age<30 的用户
@Test
public void testQuery5(){
    Query query = new Query();
    Criteria criteria = new Criteria();
    // 根据需求,此时用 and 运算符
    criteria.andOperator(
            Criteria.where("name").regex("da"),
            Criteria.where("age").gt(25).lt(30)
    );
    query.addCriteria(criteria);
    List<User> users = mt.find(query, User.class);
    users.forEach(System.err::println);
}

t.find(query, User.class);
users.forEach(System.err::println);
}

// 查询 name中含有 da 并且 25<age<30 的用户
@Test
public void testQuery5(){
Query query = new Query();
Criteria criteria = new Criteria();
// 根据需求,此时用 and 运算符
criteria.andOperator(
Criteria.where(“name”).regex(“da”),
Criteria.where(“age”).gt(25).lt(30)
);
query.addCriteria(criteria);
List users = mt.find(query, User.class);
users.forEach(System.err::println);
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值