尚医通(十)数据字典加Redis缓存 | MongoDB

一、Redis介绍

Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用ANSI c语言编写的key-value存储系统(区别于MySQL的二维表格的形式存储。)。和Memcache类似,但很大程度补偿了Memcache的不足。和Memcache一样,Redis数据都是缓存在计算机内存中,不同的是,Memcache只能将数据缓存到内存中,无法自动定期写入硬盘,这就表示,一断电或重启,内存清空,数据丢失。所以Memcache的应用场景适用于缓存无需持久化的数据。而Redis不同的是它会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,实现数据的持久化。
Redis的特点:

1,Redis读取的速度是110000次/s,写的速度是81000次/s;
2,原子 。Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
3,支持多种数据结构:string(字符串);list(列表);hash(哈希),set(集合);zset(有序集合)
4,持久化,集群部署
5,支持过期时间,支持事务,消息订阅

Spring Cache 是一个非常优秀的缓存组件。自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象,方便切换各种底层Cache(如:redis)
使用Spring Cache的好处:

1,提供基本的Cache抽象,方便切换各种底层Cache;
2,通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;
3,提供事务回滚时也自动回滚缓存;
4,支持比较复杂的缓存逻辑;

二、数据字典模块添加Redis缓存

1、service_cmn模块,添加redis依赖

<!-- redis -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X集成redis所需common-pool2-->
<dependency>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-pool2</artifactId>
     <version>2.6.0</version>
</dependency>

2、service_cmn模块,添加Redis配置类

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 设置RedisTemplate规则
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //序列号key value
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 设置CacheManager缓存规则
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();

        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

3、在service_cmn模块,配置文件添加redis配置

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0

4、通过注解添加redis缓存

@Cacheable 用在查询方法上,表示数据查询的时候,先去缓存中查询,缓存中没有再去数据库中查询
@CachePut 用在添加方法上,表示往数据中添加数据的时候,也会向缓存中添加一份
@CacheEvict 用在修改|删除方法上,表示当修改|删除数据表中数据,把缓存中对应的数据删除掉

(1)缓存@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
查看源码,属性值如下:
在这里插入图片描述
(2)缓存@CachePut
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
查看源码,属性值如下:
在这里插入图片描述
(3)缓存@CacheEvict
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
查看源码,属性值如下:
在这里插入图片描述

5、查询数据字典列表添加Redis缓存

    @ApiOperation(value = "根据数据id查询子数据列表")
    @GetMapping("/childList/{pid}")
    @Cacheable(value = "dict", key = "'selectIndexList'+#pid")
    public R getchildListById(@PathVariable Long pid){
        List<Dict> list = dictService.getchildListById(pid);
        return R.ok().data("items",list);
    }

在这里插入图片描述

6、bug(缓存清空)

如果我们插入一个一级目录
在这里插入图片描述
然后之前在redis的一级目录是没有家用电器,如果导入的话,家用电器是插入不进去的,因为redis里面有数据,不会到mysql里面查找
解决方案
在导入里面添加
在这里插入图片描述

7、总结(4大步骤)

springcache:底层redis、memcache
1.导入starter依赖
2.application.properties:redis连接信息
3.在配置类中提供一个cacheManager,在配置类上标记@EnableCaching开启缓存支持注解
4.@Cacheable(value="1111",key="'xxxxx'")   1111::xxxxx

三、MongoDB简介

1、NoSQL概述

NoSQL(NoSQL = Not Only SQL),意即反SQL运动,指的是非关系型的数据库,是一项全新的数据库革命性运动,早期就有人提出,发展至2009年趋势越发高涨。NoSQL的拥护者们提倡运用非关系型的数据存储,相对于目前铺天盖地的关系型数据库运用,这一概念无疑是一种全新的思维的注入
为什幺使用NoSQL :
1、对数据库高并发读写。
2、对海量数据的高效率存储和访问。
3、对数据库的高可扩展性和高可用性。
弱点:
1、数据库事务一致性需求
2、数据库的写实时性和读实时性需求
3、对复杂的SQL查询,特别是多表关联查询的需求

2、什么是MongoDB

MongoDB 是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。
在高负载的情况下,添加更多的节点,可以保证服务器性能。
MongoDB 旨在为WEB应用提供可扩展的高性能数据存储解决方案。
MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

在这里插入图片描述

3、MongoDB特点

1、MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。
2、你可以在MongoDB记录中设置任何属性的索引 (如:FirstName=“Sameer”,Address=“8 Gandhi Road”)来实现更快的排序。
3、你可以通过本地或者网络创建数据镜像,这使得MongoDB有更强的扩展性。
4、如果负载的增加(需要更多的存储空间和更强的处理能力),它可以分布在计算机网络中的其他节点上这就是所谓的分片。
5、Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
6、MongoDb 使用update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段。
7、Mongodb中的Map/reduce主要是用来对数据进行批量处理和聚合操作。
8、Map和Reduce。Map函数调用emit(key,value)遍历集合中所有的记录,将key与value传给Reduce函数进行处理。
9、Map函数和Reduce函数是使用Javascript编写的,并可以通过db.runCommand或mapreduce命令来执行MapReduce操作。
10、GridFS是MongoDB中的一个内置功能,可以用于存放大量小文件。
11、MongoDB允许在服务端执行脚本,可以用Javascript编写某个函数,直接在服务端执行,也可以把函数的定义存储在服务端,下次直接调用即可。
12、MongoDB支持各种编程语言:RUBY,PYTHON,JAVA,C++,PHP,C#等多种语言。
13、MongoDB安装简单。

4、MongoDB和Redis区别

MongoDBRedis
NoSQLNoSQL
不支持连表不支持连表
支持索引不支持索引
不支持事务支持事务
Bson五种常用的类型
适用于高并发的增删改查适用于高并发的读

四、MongoDB安装(docker方式)

1、拉取镜像

docker pull mongo:latest
或者
docker pull mongo:4.4.8

2、创建和启动容器

(1)对data目录授权
chmod -R 777 /data
(2)创建和启动容器

docker run -d --restart=always -p 27017:27017 --name mymongo -v /data/db:/data/db -d mongo
或者
docker run -d --restart=always -p 27017:27017 --name mymongo -v /data/db:/data/db -d mongo:4.4.8

3、进入容器

docker exec -it mymongo /bin/bash

4、使用MongoDB客户端进行操作

在这里插入图片描述

show dbs#查询所有的数据库

五、MongoDB 概念解析

不管我们学习什么数据库都应该学习其中的基础概念,在mongodb中基本的概念是文档、集合、数据库,下面我们详细介绍,下表将帮助您更容易理解Mongo中的一些概念:
在这里插入图片描述
在这里插入图片描述

1、数据库

一个mongodb中可以建立多个数据库,常用操作如下:

(1)Help查看命令提示

db.help();

(2)切换/创建数据库

use test

如果数据库不存在,则创建数据库,否则切换到指定数据库

(3) 查询所有数据库

show dbs;

(4)删除当前使用数据库

db.dropDatabase();

(5)查看当前使用的数据库

db.getName();

(6)显示当前db状态

db.stats();

(7)当前db版本

db.version();

(8) 查看当前db的链接机器地址

db.getMongo();

2、文档

文档是一组键值(key-value)对(即BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。
下表列出了 RDBMS 与 MongoDB 对应的术语:
在这里插入图片描述
需要注意的是:
1、文档中的键/值对是有序的。
2、文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
3、MongoDB区分类型和大小写。尤其是java代码
4、MongoDB的文档不能有重复的键。
5、文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。
文档键命名规范:
1、键不能含有\0 (空字符)。这个字符用来表示键的结尾。
2、.和$有特别的意义,只有在特定环境下才能使用。
3、以下划线"_"开头的键是保留的(不是严格要求的)。

3、集合

集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表格。
集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。
常用命令:
1、 创建一个集合(table)

db.createCollection( “collName”);

2、 得到指定名称的集合(table )

db.getCollection(“user”);

在这里插入图片描述

5、MongoDB适用场景

适用场景
1、网站数据:Mongo非常适合实时的插入,更新与查询,并具备网站实时数据存储所需的复制及高度伸缩性。
2、缓存:由于性能很高,Mongo也适合作为信息基础设施的缓存层。在系统重启之后,由Mongo搭建的持久化缓存层可以避免下层的数据源过载。
3、大尺寸,低价值的数据:使用传统的关系型数据库存储一些数据时可能会比较昂贵,在此之前,很多时候程序员往往会选择传统的文件进行存储。
4、高伸缩性的场景:Mongo非常适合由数十或数百台服务器组成的数据库。Mongo的路线图中已经包含对Map Reduce弓摩的内置支持。
5、用于对象及 JSON数据的存储:Mongo的BSON数据格式非常适合文档化格式的存储及查询。
不适用场合
1、高度事务性系统:例如银行系统。传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序。
2、传统的商业智能应用:针对特定问题的BI数据库会对产生高度优化的查询方式。对于此类应用,数据仓库可能是更合适的选择。

六、MongoDB常用操作(一)

1、INSERT

insert只能添加,不能做修改
save既可以做添加,也可以做修改

db.User.save({name:‘zhangsan’,age:21,sex:true})

db.User.find()

在这里插入图片描述
_id组合
Objectld是、id”的默认类型。Objectld使用12字节的存储空间,每个字节二位十六进制数字,是一个24位的字符串
在这里插入图片描述

  1. 时间戳:时间不断变化的
  2. 机器:主机的唯_标识码。通常是机器主机名的散列值,这样可以确保不同主机
    生成不同的Objectld ,不产生冲突。
  3. PID:为了确保在同一台机器上并发的多个进程产生的Objectld是唯一的,
    所以加上进程标识符(PID).
  4. 计数器:前9个字节保证了同一秒钟不同机器不同进程产生的Objectld是唯一的。
    后3个字节就是一个自动增加的计数器,确保相同进程同一秒产生的Objectld也是
    不一样。同一秒最多允许每个进程拥有IS 777 2托个不同的Objectld。

2、QUERY

2.1 WHERE

select * from User where name = ‘lucy’

db.User.find({name:“lucy”})

在这里插入图片描述

2.2 FIELDS

select name, age from User where age = 20

db.User.find({age:20}, {‘name’:1, ‘age’:1})

在这里插入图片描述

2.3 SORT

在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。

select * from User order by age

db.User.find().sort({age:1})

在这里插入图片描述
在这里插入图片描述

2.4 SUCE

在 MongoDB 中使用 limit()方法来读取指定数量的数据,skip()方法来跳过指定数量的数据
select * from User skip 2 limit 3

db.User.find().skip(0).limit(3)

在这里插入图片描述

2.5 IN

select * from User where age in (21, 26, 32)

db.User.find({age:{$in:[21,26,32]}})

在这里插入图片描述

2.6 COUNT

select count(*) from User where age >20

db.User.find({age:{$gt:20}}).count()

在这里插入图片描述

2.7 OR

select * from User where age = 20 or age = 30

db.User.find({$or:[{age:20}, {age:30}]})

在这里插入图片描述

七、MongoDB常用操作(二)

1、UPDATE

update User set age = 100, sex = 0 where name = ‘lucy’

db.User.update({name:“lucy”}, {$set:{age:100, sex:0}})

在这里插入图片描述
Update()有几个参数需要注意。
db.collection.update(criteria, objNew, upsert, mult)
criteria:需要更新的条件表达式
objNew:更新表达式
upsert:如FI标记录不存在,是否插入新文档。
multi:是否更新多个文档。

2、Remove

remove()用于删除单个或全部文档,删除后的文档无法恢复
//移除对应id的行

db.User.remove(id)
在这里插入图片描述
//移除所有

db.User.remove({})

在这里插入图片描述

3、aggregate聚合

MongoDB中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似sql语句中的 count(*)
插入测试数据

db.article.insert({
title: 'MongoDB Overview',
description: 'MongoDB is no sql database',
by_user: 'runoob.com',
url: 'http://www.runoob.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100
})

db.article.insert({
title: 'NoSQL Overview',
description: 'No sql database is very fast',
by_user: 'runoob.com',
url: 'http://www.runoob.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 10
})

db.article.insert({
title: 'Neo4j Overview',
description: 'Neo4j is no sql database',
by_user: 'Neo4j',
url: 'http://www.neo4j.com',
tags: ['neo4j', 'database', 'NoSQL'],
likes: 750
})

通过以上集合计算每个作者所写的文章数
select by_user, count(*) from article group by by_user

db.article.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : {$sum : 1}}}])

在这里插入图片描述

4、常见的聚合表达式

在这里插入图片描述

5、索引

索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。

db.User.createIndex({“name”:1})

语法中 name值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可

八、SpringBoot集成MongoDB

spring-data-mongodb提供了MongoTemplate与MongoRepository两种方式访问mongodb,MongoRepository操作简单,MongoTemplate操作灵活,我们在项目中可以灵活适用这两种方式操作mongodb,MongoRepository的缺点是不够灵活,MongoTemplate正好可以弥补不足。

1、在service_hosp引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
</dependencies>

2、添加MongoDB配置

spring.data.mongodb.uri=mongodb://192.168.121.140:27017/test

3.总结

使用MongoTmplate三步:
1.引用starter依赖
2.配置mongodb连接信息
3.在使用的地方直接注入MongoTemplate

九、基于MongoTemplate 开发CRUD

1、添加实体

添加com.donglin.yygh.hosp.testmongo.User类

@Data
@Document("User")
public class User {
    @Id //当前属性和mongodb集合中的主键是对应的
    private String id;
    private String name;
    private Integer age;
    private String email;
    private String createDate;
}

2、常用方法

常用方法
mongoTemplate.findAll(User.class): 查询User文档的全部数据
mongoTemplate.findById(< id >, User.class): 查询User文档id为id的数据
mongoTemplate.find(query, User.class);: 根据query内的查询条件查询
mongoTemplate.upsert(query, update, User.class): 修改
mongoTemplate.remove(query, User.class): 删除
mongoTemplate.insert(User): 新增
Query对象
1、创建一个query对象(用来封装所有条件对象),再创建一个criteria对象(用来构建条件)
2、 精准条件:criteria.and(“key”).is(“条件”)
模糊条件:criteria.and(“key”).regex(“条件”)
3、封装条件:query.addCriteria(criteria)
4、大于(创建新的criteria):Criteria gt = Criteria.where(“key”).gt(“条件”)
小于(创建新的criteria):Criteria lt = Criteria.where(“key”).lt(“条件”)
5、Query.addCriteria(new Criteria().andOperator(gt,lt));
6、一个query中只能有一个andOperator()。其参数也可以是Criteria数组。
7、排序 :query.with(new Sort(Sort.Direction.ASC, “age”). and(new Sort(Sort.Direction.DESC, “date”)))

3、添加测试类

@RestController
@RequestMapping("/mongo1")
public class TestMongo1 {

    @Autowired
    private MongoTemplate mongoTemplate;

    //添加
    @GetMapping("create")
    public void createUser() {
        User user = new User();
        user.setAge(20);
        user.setName("test");
        user.setEmail("4932200@qq.com");
        User user1 = mongoTemplate.insert(user);
        System.out.println(user1);
    }
    //1.insert与save区别:insert只能做添加,save既可以做添加也可以做修改操作
    //2.save做修改时,必须先查询,然后修改才行,不能直接修改。
    //3.批量添加只能用insert

    //查询所有
    @GetMapping("findAll")
    public void findUser() {
        List<User> userList = mongoTemplate.findAll(User.class);
        System.out.println(userList);
    }

    //根据id查询
    @GetMapping("findId")
    public void getById() {
        User user =
                mongoTemplate.findById("5ffbfa2ac290f356edf9b5aa", User.class);
        System.out.println(user);
    }

    //条件查询
    @GetMapping("findUser")
    public void findUserList() {
        Query query = new Query(Criteria
                .where("name").is("test")
                .and("age").is(20));
        List<User> userList = mongoTemplate.find(query, User.class);
        System.out.println(userList);
    }

    //模糊查询
    @GetMapping("findLike")
    public void findUsersLikeName() {
        String name = "est";
        String regex = String.format("%s%s%s", "^.*", name, ".*$");
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        Query query = new Query(Criteria.where("name").regex(pattern));
        List<User> userList = mongoTemplate.find(query, User.class);
        System.out.println(userList);
    }

    //分页查询
    @GetMapping("findPage")
    public void findUsersPage() {
        String name = "est";
        int pageNo = 1;
        int pageSize = 10;

        Query query = new Query();
        String regex = String.format("%s%s%s", "^.*", name, ".*$");
        Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        query.addCriteria(Criteria.where("name").regex(pattern));
        int totalCount = (int) mongoTemplate.count(query, User.class);
        List<User> userList = mongoTemplate.find(query.skip((pageNo - 1) * pageSize).limit(pageSize), User.class);

        Map<String, Object> pageMap = new HashMap<>();
        pageMap.put("list", userList);
        pageMap.put("totalCount",totalCount);
        System.out.println(pageMap);
    }

    //修改
    @GetMapping("update")
    public void updateUser() {
     
        Query query = new Query(Criteria.where("_id").is(user.getId()));
        Update update = new Update();
        update.set("name", user.getName());
        update.set("age", user.getAge());
        update.set("email", user.getEmail());
        UpdateResult result = mongoTemplate.upsert(query, update, User.class);
        long count = result.getModifiedCount();
        System.out.println(count);
    }
    //upsert:可以做修改或者添加操作
    //updateFirst:
    //updateMulti:

    //删除操作
    @GetMapping("delete")
    public void delete() {
        Query query =
                new Query(Criteria.where("_id").is("5ffbfa2ac290f356edf9b5aa"));
        DeleteResult result = mongoTemplate.remove(query, User.class);
        long count = result.getDeletedCount();
        System.out.println(count);
    }
    //mongo中是区分大小写和数据类型的。
    
    //Criteria也可以构成 "或" 的条件
    Criteria criteria=new Criteria();
    criteria.orOperator(Criteria,Criteria);
}

十、基于MongoRepository开发CRUD

Spring Data提供了对mongodb数据访问的支持,我们只需要继承MongoRepository类,按照Spring Data规范就可以了
SpringData 方法定义规范
在这里插入图片描述
在这里插入图片描述
1、不是随便声明的,而需要符合一定的规范
2、 查询方法以find | read | get开头
3、 涉及条件查询时,条件的属性用条件关键字连接
4、 要注意的是:条件属性首字母需要大写
5、 支持属性的级联查询,但若当前类有符合条件的属性则优先使用,而不使用级联属性,若需要使用级联属性,则属性之间使用_强制进行连接

1、添加Repository类

@Repository
public interface UserRepository extends MongoRepository<User, String> {
}

2、编写测试类

@RestController
@RequestMapping("/mongo2")
public class TestMongo2 {

    @Autowired
    private UserRepository userRepository;

    //添加
    @GetMapping("create")
    public void createUser() {
        User user = new User();
        user.setAge(20);
        user.setName("张三");
        user.setEmail("3332200@qq.com");
        User user1 = userRepository.save(user);
    }
    //1.insert与save方法可以做添加操作
    //2.save也可以做修改操作
    //3.insert可以同时添加多条数据,saveAll也可以
    
    //先查询出来再更新,否则出问题

    //查询所有
    @GetMapping("findAll")
    public void findUser() {
        List<User> userList = userRepository.findAll();
        System.out.println(userList);
    }

    //id查询
    @GetMapping("findId")
    public void getById() {
        User user = userRepository.findById("60b8d57ed539ed5b124942de").get();
        System.out.println(user);
    }

    //条件查询
    @GetMapping("findQuery")
    public void findUserList() {
        User user = new User();
        user.setName("张三");
        user.setAge(20);
        Example<User> userExample = Example.of(user);
        List<User> userList = userRepository.findAll(userExample);
        System.out.println(userList);
    }

    //模糊查询
    @GetMapping("findLike")
    public void findUsersLikeName() {
        //创建匹配器,即如何使用查询条件
        ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
                .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
                .withIgnoreCase(true); //改变默认大小写忽略方式:忽略大小写
        User user = new User();
        user.setName("三");
        Example<User> userExample = Example.of(user, matcher);
        List<User> userList = userRepository.findAll(userExample);
        System.out.println(userList);
    }

    //分页查询
    @GetMapping("findPage")
    public void findUsersPage() {
        Sort sort = Sort.by(Sort.Direction.DESC, "age");
        //0为第一页
        Pageable pageable = PageRequest.of(0, 10, sort);
        //创建匹配器,即如何使用查询条件
        ExampleMatcher matcher = ExampleMatcher.matching() //构建对象
                .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) //改变默认字符串匹配方式:模糊查询
                .withIgnoreCase(true); //改变默认大小写忽略方式:忽略大小写
        User user = new User();
        user.setName("三");
        Example<User> userExample = Example.of(user, matcher);
        //创建实例
        Example<User> example = Example.of(user, matcher);
        Page<User> pages = userRepository.findAll(example, pageable);
        System.out.println(pages);
    }

    //修改
    @GetMapping("update")
    public void updateUser() {
        User user = userRepository.findById("60b8d57ed539ed5b124942de").get();
        user.setName("张三_1");
        user.setAge(25);
        user.setEmail("883220990@qq.com");
        User save = userRepository.save(user);
        System.out.println(save);
    }

    //删除
    @GetMapping("delete")
    public void delete() {
        userRepository.deleteById("60b8d57ed539ed5b124942de");
    }
    //delete只能根据id删除,就算设置其它条件而不设置id,也是删除不了的。
}

3、SpringData方法规范

@GetMapping("testMethod2")
public void testMethod2() {
    List<User> users = userRepository.findByNameLike("张");
    System.out.println(users);
}

@GetMapping("testMethod1")
public void testMethod1() {
    List<User> users = userRepository.findByName("张三");
    System.out.println(users);
}

@Repository
public interface UserRepository extends MongoRepository<User, String> {

    List<User> findByName(String name);

    List<User> findByNameLike(String name);
}
<think>我们遇到了错误:`Cannotreadpropertiesofundefined(reading'fetch')`。这个错误常发生在尝试访问一个未定义对象的`fetch`属性时。在之前的代码中,我们使用了`pdf.xref.fetch`,但有时`pdf.xref`可能未定义。问题分析:1.`getDocument()`返回的PDF文档对象在载完成之前,其内部结构(如xref)可能还未完全初始化。2.可能由于PDF文档损坏或载不完整导致`xref`不可用。解决方案:-确保在PDF文档完全载后才访问`xref`。-使用`getXref()`方法代替直接访问`xref`属性(在pdfjs-distv3.x中推荐方式)。修改后的图片提取函数:```javascriptimport{getDocument,GlobalWorkerOptions,OPS}from'pdfjs-dist/build/pdf';//设置worker源GlobalWorkerOptions.workerSrc='https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js';asyncfunctionextractImages(pdfUrl,containerId){constcontainer=document.getElementById(containerId);try{constpdfDoc=awaitgetDocument(pdfUrl).promise;for(letpageNum=1;pageNum<=pdfDoc.numPages;pageNum++){constpage=awaitpdfDoc.getPage(pageNum);//获取交叉引用表,推荐使用getXref方法constxref=awaitpdfDoc.getXref();constoperatorList=awaitpage.getOperatorList();//遍历操作符列表for(leti=0;i< operatorList.fnArray.length;i++){if(operatorList.fnArray[i]===OPS.paintImageXObject){constimgKey=operatorList.argsArray[i][0];//使用xref.fetchconstres=awaitxref.fetch(imgKey);constimgData=resinstanceofStream?res.getBytes():res;//创建图片元素constimg=document.createElement('img');img.src=URL.createObjectURL(newBlob([imgData],{type:'image/jpeg'}));container.appendChild(img);}}}}catch(error){console.error('Errorextractingimages:',error);}}```但是,在pdfjs-distv3.x中,`pdfDoc`对象没有直接暴露`getXref`方法,而是过`pdfDoc._pdfInfo.xref`访问(但这是内部属性,不建议使用)。因此,更安全的方式是使用`pdfDoc.commonObjs`和`pdfDoc.objs`来获取资源。实际上,在PDF.js中提取图片的标准做法是使用`getOperatorList`然后解析图片操作,但直接过操作符列表提取图片可能会遇到一些问题,因为图片可能被缓存或存储在公共对象中。因此,我们采用另一种更可靠的方式:使用`page.getOperatorList`并配合`PDFOperatorList`的事件处理来提取图片。###推荐方法:使用`page.getOperatorList`的回调方式```javascriptasyncfunctionextractImages(pdfUrl,containerId){constcontainer=document.getElementById(containerId);constpdfDoc=awaitgetDocument(pdfUrl).promise;for(letpageNum=1;pageNum<=pdfDoc.numPages;pageNum++){constpage=awaitpdfDoc.getPage(pageNum);//获取操作符列表,并设置回调处理图片awaitpage.getOperatorList({handler:(handler)=>{handler._handlers=[];//初始化//监听图片绘制操作handler.on('paintImageXObject',(operator,args)=>{constimgKey=args[0];//获取图像对象constxobjs=handler.commonObjs.get(imgKey);if(xobjs){//获取图像数据(base64)constimg=document.createElement('img');img.src=xobjs;container.appendChild(img);}else{console.warn('Imagenotfoundincommonobjects:',imgKey);}});}});}}```然而,上述方式在PDF.js的最新版本中并不直接支持。因此,我们采用官方示例中的方法:在渲染页面时捕获图片。###替代方案:在渲染到Canvas时提取图片PDF.js在渲染页面时已经处理了所有图片,我们可以过修改Canvas渲染器来捕获图片。但是,这需要我们自定义渲染过程。###实际可行的方案:使用官方推荐的`PDFPageProxy.getViewport`和`render`方法如果我们的目标只是显示PDF并提取图片,那么可以在渲染Canvas时过重写图片处理函数来捕获图片。但注意,这需要深入PDF.js内部。###简易方案:使用`pdf.js`自带的`getData`方法由于直接提取图片相对复杂,我们可以考虑使用官方示例中的[示例](https://github.com/mozilla/pdf.js/blob/master/examples/node/getimages.js)的浏览器版本。以下是结合官方示例的浏览器端图片提取代码:```javascriptimport{getDocument}from'pdfjs-dist/build/pdf';GlobalWorkerOptions.workerSrc='https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js';asyncfunctionextractImages(pdfUrl,containerId){constcontainer=document.getElementById(containerId);constloadingTask=getDocument(pdfUrl);constpdf=awaitloadingTask.promise;for(leti=1;i<=pdf.numPages;i++){constpage=awaitpdf.getPage(i);constcontent=awaitpage.getOperatorList();constobjs=content.objs;for(letj=0;j< objs.length;j++){if(objs[j]instanceofPDFImage){//创建图像constimg=newImage();img.src=awaitobjs[j].createImageData();container.appendChild(img);}}}}```但是,在浏览器端,`PDFImage`没有直接导出,因此我们需要从`pdfjs-dist`中导入`PDFImage`。###目前最可靠的方式(使用pdfjs-distv2.6+):在v3.4.120中,我们可以使用以下方式:```javascriptimport{getDocument,GlobalWorkerOptions,ImageKind}from'pdfjs-dist/build/pdf';asyncfunctionextractImages(pdfUrl,containerId){constcontainer=document.getElementById(containerId);constpdfDoc=awaitgetDocument(pdfUrl).promise;for(leti=1;i<=pdfDoc.numPages;i++){constpage=awaitpdfDoc.getPage(i);constoperators=awaitpage.getOperatorList();//遍历操作符for(letj=0;j< operators.fnArray.length;j++){if(operators.fnArray[j]===OPS.paintImageXObject){constimageName=operators.argsArray[j][0];//从页面的资源中获取图像constxobjs=awaitpage.getXObject(imageName);if(xobjsinstanceofPDFImage){constimg=newImage();img.src=URL.createObjectURL(awaitxobjs.getImageData());container.appendChild(img);}}}}}```###最终解决方案(适配pdfjs-distv3.x):遗憾的是,在v3.x中,内部API发生了很大变化,上述方法可能不工作。因此,我推荐使用`pdf.js`的`page.getImages()`方法。```javascriptimport{getDocument,GlobalWorkerOptions}from'pdfjs-dist/build/pdf';GlobalWorkerOptions.workerSrc='https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js';asyncfunctionextractImages(pdfUrl,containerId){constcontainer=document.getElementById(containerId);try{constpdfDoc=awaitgetDocument(pdfUrl).promise;for(letpageNum=1;pageNum<=pdfDoc.numPages;pageNum++){constpage=awaitpdfDoc.getPage(pageNum);constimages=awaitpage.getImages();for(constimgofimages){//获取图像对象constxobj=awaitimg.obj;//获取图像数据constimageData=awaitxobj.getImageData();constimgElement=document.createElement('img');imgElement.src=URL.createObjectURL(newBlob([imageData.buffer],{type:imageData.mimeType||'image/jpeg'}));container.appendChild(imgElement);}}}catch(error){console.error('Errorextractingimages:',error);}}```请注意:在pdfjs-distv3.x中,`page.getImages()`返回一个数组,数组中的每个元素包含图像的引用(obj),过`obj.getImageData()`可以获取图像的像素数据(ImageData对象)以及元数据。但是,ImageData对象并不能直接作为图片src。因此,我们需要将其转换为base64或BlobURL。但是,`xobj.getImageData()`返回的是包含`width`,`height`,`data`的ImageData类型,但`data`是Uint8Array,我们需要将其转换为图片格式(JPEG/PNG)。我们可以过将ImageData绘制到Canvas上,然后将Canvas转换为DataURL。###修改:过Canvas转换ImageData为DataURL```javascript//...在获取到imageData后constcanvas=document.createElement('canvas');canvas.width=imageData.width;canvas.height=imageData.height;constctx=canvas.getContext('2d');ctx.putImageData(imageData,0,0);constdataUrl=canvas.toDataURL('image/jpeg');//或者'image/png'imgElement.src=dataUrl;```然而,这可能会遇到跨域问题,并且性能较低。所以,我们尝试直接从图像的原始字节数据创建Blob。###优化:使用原始字节数据实际上,`xobj.getImageData()`返回的ImageData的`data`属性是解码后的像素数据(RGBA),而不是原始压缩的图像数据。所以,如果我们想要原始图像数据(比如JPEG或PNG的字节流),应该使用`xobj.getImageBytes()`,但这个方法在浏览器端可能没有。因此,对于包含压缩图像数据的PDF图片,我们可以直接从`xobj`获取:```javascriptimport{getDocument,GlobalWorkerOptions}from'pdfjs-dist/build/pdf';GlobalWorkerOptions.workerSrc='https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js';asyncfunctionextractImages(pdfUrl,containerId){constcontainer=document.getElementById(containerId);try{constpdfDoc=awaitgetDocument(pdfUrl).promise;for(letpageNum=1;pageNum<=pdfDoc.numPages;pageNum++){constpage=awaitpdfDoc.getPage(pageNum);constimages=awaitpage.getImages();for(constimageofimages){constimgObj=image.obj;//获取图像数据流conststream=imgObjinstanceofPDFStream?imgObj:imgObj.get('SMask')||imgObj;constbytes=awaitstream.getBytes();//获取图像类型letmimeType;constimgType=imgObj.dict.get('Subtype').name;if(imgType==='Image'){constfilter=imgObj.dict.get('Filter');if(filter){if(filter.name==='DCTDecode'){mimeType='image/jpeg';}elseif(filter.name==='JPXDecode'){mimeType='image/jp2';}elseif(filter.name==='FlateDecode'){mimeType='image/png';}else{//未知类型,尝试解码mimeType='image/png';}}}//创建BlobURLconstblob=newBlob([bytes],{type:mimeType});consturl=URL.createObjectURL(blob);constimgElement=document.createElement('img');imgElement.src=url;container.appendChild(imgElement);}}}catch(error){console.error('Errorextractingimages:',error);}}```注意:这种方法适用于原始图像数据被压缩存储的情况,但是对于由PDF指令绘制的图片(如FormXObject)可能不适用。另外,对于包含透明道的图片,可能需要特殊处理。###总结1.使用`page.getImages()`获取图像列表。2.对每个图像对象,获取其原始字节流。3.根据图像在PDF中的压缩格式确定MIME类型。4.创建BlobURL并生成`<img>`元素。由于PDF中图像格式的复杂性,此方法可能无法覆盖所有情况。因此,建议在实际应用中进行充分测试。§§相关问题§§:1.如何判断PDF中的图片是否带有透明道?2.在PDF.js中如何处理FormXObject(表单对象)中的图片?3.提取PDF图片时如何处理颜色空间(ColorSpace)的转换?4.如何提高从PDF中提取大量图片的性能?</think> # 解决 "Cannot read properties of undefined (reading 'fetch')" 错误 这个错误发生在您尝试访问 `pdf.xref.fetch()` 时,表明 `pdf.xref` 是 `undefined`。当PDF文档没有正确载或PDF.js内部结构变更时会出现这个问题。以下是完整的解决方案: ## 原因分析 1. PDF文档载失败或未完成初始化 2. PDF.js内部API变更(特别是xref结构变更) 3. 浏览器安全限制导致资源载失败 ## 解决方案:使用安全的图像提取方法 ```javascript import { getDocument, GlobalWorkerOptions, OPS } from 'pdfjs-dist/build/pdf'; import 'pdfjs-dist/build/pdf.worker.entry'; // 设置worker源 GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.js'; async function extractImages(pdfUrl, containerId) { try { const container = document.getElementById(containerId); const pdfDoc = await getDocument(pdfUrl).promise; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); // 安全方法:直接访问PDF页面资源 const resources = await page.getResources(); const xobjs = resources.get('XObject'); if (!xobjs) continue; const xobjIds = xobjs.keys(); for (const xobjId of xobjIds) { const obj = await xobjs.get(xobjId); if (obj.type !== 'XObject' || obj.subtype !== 'Image') continue; try { const imgData = await obj.getImageData(); const mimeType = getImageType(obj); const blob = new Blob([imgData.buffer], { type: mimeType }); const img = document.createElement('img'); img.src = URL.createObjectURL(blob); img.alt = `PDF Image ${pageNum}-${xobjId}`; container.appendChild(img); // 清理内存 img.onload = () => URL.revokeObjectURL(img.src); } catch (e) { console.warn(`Failed to extract image ${xobjId}`, e); } } } } catch (error) { console.error('PDF processing failed:', error); } } // 检测图片类型 function getImageType(imageObject) { const filter = imageObject.dict.get('Filter'); if (filter) { if (filter.name === 'DCTDecode') return 'image/jpeg'; if (filter.name === 'JPXDecode') return 'image/jp2'; if (filter.name === 'CCITTFaxDecode') return 'image/tiff'; } return 'image/png'; // 默认格式 } // 使用示例 extractImages('document.pdf', 'image-container'); ``` ## 替代解决方案:使用可靠的中介Canvas ```javascript async function extractViaCanvas(pdfUrl, containerId) { try { const container = document.getElementById(containerId); const pdfDoc = await getDocument(pdfUrl).promise; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: 2 }); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.height = viewport.height; canvas.width = viewport.width; // 1. 渲染整个页面到canvas await page.render({ canvasContext: context, viewport }).promise; // 2. 使用Canvas获取所有图像元素 const imgElements = canvas.getElementsByTagName('img'); if (imgElements.length === 0) continue; // 3. 克隆并提取图像 Array.from(imgElements).forEach(img => { container.appendChild(img.cloneNode()); }); // 清理中间canvas canvas.remove(); } } catch (error) { console.error('Canvas extraction failed:', error); } } ``` ## 关键改进说明: 1. **安全的PDF对象访问**: - 使用 `getResources()` 替代直接访问 `xref` - 过 `get('XObject')` 获取图像资源列表 2. **健壮的错误处理**: - 全面的try/catch保护 - 跳过不可用资源 - 处理各种图像格式 3. **API兼容性**: - 使用PDF.js官方支持的API - 避免访问内部对象如 `_objs` 或 `argsArray` 4. **内存管理**: - 使用 `URL.revokeObjectURL()` 释放内存 - 及时清理中间canvas对象 ## 高级解决方案:扩展页面资源提取 ```javascript async function extractAllResources(pdfUrl, containerId) { const pdfDoc = await getDocument(pdfUrl).promise; const container = document.getElementById(containerId); for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); const resources = await page.getResources(); const fonts = resources.get('Font'); const images = resources.get('XObject'); const pageDiv = document.createElement('div'); pageDiv.className = 'pdf-page'; pageDiv.innerHTML = `<h3>Page ${pageNum} Resources</h3>`; // 提取图像 if (images) { const imgSection = document.createElement('div'); imgSection.innerHTML = '<h4>Images</h4>'; const imgIds = images.keys(); for (const imgId of imgIds) { const imgObj = await images.get(imgId); if (imgObj.subtype !== 'Image') continue; try { const imgData = await imgObj.getImageData(); const img = document.createElement('img'); img.src = URL.createObjectURL(new Blob([imgData.buffer])); img.alt = `Page ${pageNum} - ${imgId}`; imgSection.appendChild(img); } catch (e) { console.warn(`Failed to extract image ${imgId}`, e); } } pageDiv.appendChild(imgSection); } // 提取字体 (可选) if (fonts) { const fontSection = document.createElement('div'); fontSection.innerHTML = '<h4>Fonts</h4><ul></ul>'; const fontIds = fonts.keys(); for (const fontId of fontIds) { const li = document.createElement('li'); try { const fontObj = await fonts.get(fontId); li.textContent = `${fontId}: ${fontObj.name} ${fontObj.type}`; } catch (e) { li.textContent = `${fontId}: unavailable`; } fontSection.querySelector('ul').appendChild(li); } pageDiv.appendChild(fontSection); } container.appendChild(pageDiv); } } ``` ## 预防性措施: 1. **检查PDF载状态**: ```javascript const pdfDoc = await getDocument(pdfUrl).promise; if (!pdfDoc.loaded) { throw new Error('PDF document is not fully loaded'); } ``` 2. **验证API可用性**: ```javascript if (!pdfDoc.getPage || typeof pdfDoc.getPage !== 'function') { throw new Error('PDF.js API has changed'); } ``` 3. **使用兼容版本**: ```bash npm install [email protected] ``` 4. **启用调试**: ```javascript import { setLogLevel, VERBOSITY_LEVELS } from 'pdfjs-dist/lib/shared/util'; setLogLevel(VERBOSITY_LEVELS.infos); ``` ## 完整的工作流程: 1. 验证PDF载成功 2. 使用安全API访问资源字典 3. 识别所有XObject图像资源 4. 直接提取图像原始数据 5. 根据图像类型创建Blob 6. 渲染到页面并清理内存
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Knight_AL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值