当前位置: 首页 > article >正文

记录一次关于spring映射postgresql的jsonb类型的转化器事故,并使用hutool的JSONArray完成映射

事件的起因是这样的,那次事故发生的起因是因为WebFlux和postgreSQL去重新做鱼皮的鱼图图项目(鱼图图作业)。

在做到picture表的时候,发现postgreSQL中有个jsonb的类型可以更好的支持json数组。

出于锻炼新技术的目的,本人过于自信的选择选择了jsonb格式类型,谁知道是长达两天的折磨,不过好在最后是解决,这个问题了,主要是当时出的问题在网上一直没有查询到关键的解法,打算写一篇博客记录一下,为后面的新技术开发提供经验。

jsonb在Java中对应的映射类型

这个在网上非常好查到

io.r2dbc.postgresql.codec.Json

使用此类型作为映射的字段即可,这也是我的第一版实体类(故事开始了)

package com.yupi.yunpicturebackend.model.entity.picture;

import com.yupi.yunpicturebackend.common.r2dbc.BaseEntity;
import io.r2dbc.postgresql.codec.Json;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

import java.time.LocalDateTime;

@Table("picture")
@Data
@Accessors(chain = true)
public class Picture
		extends BaseEntity {
	@Id
	private String id;//*图片id
	private String url;//*图片url
	private String name;//*图片名称
	private String introduction;//图片简介
	private String category;//图片分类
	private Json tags;//图片标签
	private Long picSize;//图片大小
	private Integer picWidth;//图片宽度
	private Integer picHeight;//图片高度
	private Double picScale;//图片比例
	private String picFormat;//图片格式
	private String userId;//*用户id
	private LocalDateTime createTime;//*创建时间
	private LocalDateTime editTime;//*编辑时间
	private LocalDateTime updateTime;//*更新时间
	private Boolean isDelete;//是否删除
}

这里使用String作为id的映射类是因为这里的id设置的是uuid。

然后简单的写完spring-data对于R2DBC的支持及项目的服务类,ReactBaseRepository是我自己对于R2dbcRepository的封装,ReactBaseService,ReactBaseServiceImpl,BaseEntity 是对于ReactBaseRepository业务一些默认泛型处理,包括分页条件查询什么的,在踩完jsonb这个坑的时候仅支持id为String类型的泛型分页查询,某条件下的数量统计,尚不完善,如果完善的话就再写一篇博客,没有大概率是鸽了【狗头】。

package com.yupi.yunpicturebackend.repositories;

import com.yupi.yunpicturebackend.common.r2dbc.repository.ReactBaseRepository;
import com.yupi.yunpicturebackend.model.entity.picture.Picture;
import org.springframework.data.r2dbc.repository.Modifying;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;

@Repository
public interface PictureRepository
		extends ReactBaseRepository<Picture, String> {
    @Override
	@Modifying
	@Query("UPDATE picture SET is_delete = true WHERE id = :#{#id}")
	Mono<Integer> deleteByLogicId(@Param("id") String id);
}
package com.yupi.yunpicturebackend.service;

import com.yupi.yunpicturebackend.common.r2dbc.service.ReactBaseService;
import com.yupi.yunpicturebackend.model.entity.picture.Picture;
import com.yupi.yunpicturebackend.repositories.PictureRepository;

public interface PictureService
		extends ReactBaseService<Picture, PictureRepository> {
}
package com.yupi.yunpicturebackend.service.impl;

import com.yupi.yunpicturebackend.common.r2dbc.service.impl.ReactBaseServiceImpl;
import com.yupi.yunpicturebackend.model.entity.picture.Picture;
import com.yupi.yunpicturebackend.repositories.PictureRepository;
import com.yupi.yunpicturebackend.service.PictureService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Service;

@Service
public class PictureServiceImpl
		extends ReactBaseServiceImpl<Picture, PictureRepository>
		implements PictureService {
	private final Picture picture_void = new Picture();
	
	private final Logger log = LoggerFactory.getLogger(PictureService.class);
	
	@Autowired
	public PictureServiceImpl(R2dbcEntityTemplate template,
	                          PictureRepository r2dbcRepository) {
		super(template,
		      r2dbcRepository);
	}
}

写一个简单的测试方法试一下

@SpringBootTest
public class PictureTestApplication {
    @Test
	public void 图片测试_新增() throws IOException {
		PictureService pictureService = SpringUtil.getBean(PictureService.class);
		JSONArray tags = JSONUtil.parseArray("[\"标签3\",\"标签4\"]");
		System.out.println(Json.of(tags.toString()));
		pictureService
				.save(new Picture()
						      .setUrl("测试图片地址")
						      .setName(RandomUtils.generateUniqueName())
						      .setUserId("1")
						      .setCreateTime(LocalDateTime.now())
						      .setUpdateTime(LocalDateTime.now())
						      .setEditTime(LocalDateTime.now())
						      .setTags(Json.of(tags.toString()))
				)
				.subscribe(System.out::println);
		System.in.read();
	}
}

这里的save是我在ReactBaseServiceImpl的一个实现,可以理解为调用了ReactBaseRepository的save方法,RandomUtils.generateUniqueName()是本人制作的一个小玩意,如下所示,效果大概是获取一个伪随机的名字。

package com.yupi.yunpicturebackend.utils;

import cn.hutool.core.util.RandomUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class RandomUtils {
	// 用于存储已经生成的名字,确保不重复
	private static final List<String> generatedNames = new ArrayList<>();
	
	// 修饰词列表
	private static final String[] adjectives = {
			"勇敢", "聪明", "神秘", "强大", "古老", "美丽", "温柔", "快速", "坚硬", "灵活",
			"炽热", "冰冷", "光明", "黑暗", "梦幻", "现实", "华丽", "朴素", "高贵", "平凡",
			"稀有", "常见", "珍贵", "普通", "完美", "残缺", "幸运", "不幸", "欢乐", "悲伤",
			"幸福", "痛苦", "寂静", "喧闹", "遥远", "接近", "巨大", "微小", "繁多", "单一",
			"锋利", "钝拙", "深邃", "浅显", "真实", "虚假", "神圣", "邪恶", "忠诚", "背叛",
			// ... 可以继续添加更多修饰词
	};
	
	// 语气词列表
	private static final String[] particles = {"的","の"};
	
	// 名字来源列表
	private static final String[] items = {
			"剑", "盾", "弓", "箭", "书", "画", "琴", "棋", "茶", "酒",
			"花", "草", "木", "石", "玉", "金", "银", "铜", "铁", "锡",
			// ... 可以继续添加更多物品名
	};
	
	private static final String[] animals = {
			"龙", "凤", "虎", "豹", "狮", "象", "狼", "熊", "狐", "鹿",
			"鹰", "鸽", "蛇", "龟", "兔", "鼠", "猫", "狗", "马", "牛",
			// ... 可以继续添加更多动物名
	};
	
	private static final String[] mythologicalFigures = {
			"奥丁", "宙斯", "赫拉", "阿波罗", "雅典娜", "波塞冬", "哈迪斯", "狄俄尼索斯",
			"赫尔墨斯", "阿瑞斯", "阿佛洛狄忒", "赫菲斯托斯", "阿尔忒弥斯", "潘", "美杜莎",
			// ... 可以继续添加更多神话人物名
	};
	
	private static final String[] sciFiCharacters = {
			"机器人", "外星人", "赛博格", "宇航员", "科学家", "探险家", "飞行员", "时空旅行者",
			"未来战士", "星际商人", "克隆人", "虚拟偶像", "AI助手", "基因改造人", "数字幽灵",
			// ... 可以继续添加更多科幻虚构人物名
	};
	
	public static String generateUniqueName() {
		String name;
		do {
			name = createRandomName();
		} while (generatedNames.contains(name) && generatedNames.size() < 1000);
		
		generatedNames.add(name);
		return name;
	}
	
	private static String createRandomName() {
		StringBuilder nameBuilder = new StringBuilder();
		// 随机选择一个修饰词
		nameBuilder.append(RandomUtil.randomEle(adjectives));
		// 随机选择一个语气词
		if (RandomUtil.randomBoolean()) nameBuilder.append(RandomUtil.randomEle(particles));
		// 随机选择一个名字来源
		nameBuilder.append(RandomUtil.randomEle(RandomUtil.randomEle(Arrays.asList(items, animals, mythologicalFigures, sciFiCharacters))));
		// 判断是否含有语气助词
		if (!Arrays.stream(particles).anyMatch(nameBuilder.toString()::contains)){
			if (RandomUtil.randomBoolean()) {
				nameBuilder.append(RandomUtil.randomEle(particles));
				nameBuilder.append(RandomUtil.randomEle(RandomUtil.randomEle(Arrays.asList(items, animals, mythologicalFigures, sciFiCharacters))));
			}
		}
		return nameBuilder.toString();
	}
	
	
}

点击运行,可以看见运行结果,这里spring的日志配置是如下所示,所以会打印一些r2dbc的日志。

logging:
  level:
    org.springframework.data.r2dbc: DEBUG
    io.r2dbc.postgresql.QUERY: DEBUG # for queries

 运行结果如下所示,写这篇踩坑心得之前已经踩完坑了,所以数据库不是空的。

 那么再按照鱼皮的用法,再封装一个vo中间键就大工完成了。

package com.yupi.yunpicturebackend.model.entity.picture.vo;

import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.yupi.yunpicturebackend.model.entity.picture.Picture;
import com.yupi.yunpicturebackend.model.entity.user.vo.SysUserVo;
import io.r2dbc.postgresql.codec.Json;
import lombok.Data;
import org.springframework.beans.BeanUtils;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class PictureVO
		implements Serializable {
	
	private static final long serialVersionUID = 1L;
	/**
	 * id
	 */
	private String id;
	/**
	 * 图片 url
	 */
	private String url;
	/**
	 * 图片名称
	 */
	private String name;
	/**
	 * 简介
	 */
	private String introduction;
	/**
	 * 标签
	 */
	private JSONArray tags;
	/**
	 * 分类
	 */
	private String category;
	/**
	 * 文件体积
	 */
	private Long picSize;
	/**
	 * 图片宽度
	 */
	private Integer picWidth;
	/**
	 * 图片高度
	 */
	private Integer picHeight;
	/**
	 * 图片比例
	 */
	private Double picScale;
	/**
	 * 图片格式
	 */
	private String picFormat;
	/**
	 * 用户 id
	 */
	private Long userId;
	/**
	 * 创建时间
	 */
	private LocalDateTime createTime;
	/**
	 * 编辑时间
	 */
	private LocalDateTime editTime;
	/**
	 * 更新时间
	 */
	private LocalDateTime updateTime;
	/**
	 * 创建用户信息
	 */
	private SysUserVo user;
	
	/**
	 * 封装类转对象
	 */
	public static Picture voToObj(PictureVO pictureVO) {
		if (pictureVO == null) {
			return null;
		}
		Picture picture = new Picture();
		BeanUtils.copyProperties(pictureVO,
		                         picture);
		//类型不同,需要转换  
		picture.setTags(Json.of(JSONUtil.toJsonStr(pictureVO.getTags())));
		return picture;
	}
	
	/**
	 * 对象转封装类
	 */
	public static PictureVO objToVo(Picture picture) {
		if (picture == null) {
			return null;
		}
		PictureVO pictureVO = new PictureVO();
		BeanUtils.copyProperties(picture,
		                         pictureVO);
		// 类型不同,需要转换  
		if (picture.getTags() != null)
			pictureVO.setTags(JSONUtil.parseArray(picture.getTags()
			                                             .asString()));
		return pictureVO;
	}
}

但是对于这样的结果我不是很认可,至少在我向它妥协之前是这样的,在我看来应该避免io.r2dbc.postgresql.codec.Json 在实体类中的出现,以为它不是被java语法或者java 8语法定义的,我看了一下它的底层,是对于一个字节流的封装,这就导致类似hutool工具对于它的支持也很少,在项目完工后它一定只会在类似dao层接受数据的地方出现,从逻辑上来说他不该作为实体类。而且对应每一个含有io.r2dbc.postgresql.codec.Json对象的实体类都做一次转换成vo,调用一次hutool工具的copy bean方法也是有不小的工作量。

根据我在b站学习了一个小时左右的webflux经验来看,我们需要定义一个转换器(天坑)

jsonb转换器

这里网上的写法有很多,出于方便以后我回忆故事,这里我只展示最终版本的代码以及中间踩了什么坑。

package com.yupi.yunpicturebackend.config;


import com.yupi.yunpicturebackend.common.r2dbc.converter.json.JsonArrayToJson;
import com.yupi.yunpicturebackend.common.r2dbc.converter.json.JsonToJsonArray;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;

import java.util.ArrayList;
import java.util.List;


@EnableR2dbcRepositories(basePackages = "com.yupi.yunpicturebackend.repositories")//开启R2DBC的仓库功能
@Configuration
public class R2DBCConfig {
	
	@Bean
	public R2dbcCustomConversions customConversions() {
		List<Object> converters = new ArrayList();
		converters.add(new JsonArrayToJson());
		converters.add(new JsonToJsonArray());
		return R2dbcCustomConversions.of(
				PostgresDialect.INSTANCE,
				converters
		);
	}
}
@WritingConverter
public class JsonArrayToJson implements Converter<JSONArray, Json> {
	@Override
	public Json convert(JSONArray source) {
		return Json.of(source.toString());
	}
}
@ReadingConverter
public class JsonToJsonArray
		implements Converter<Json, JSONArray> {
	@Override
	public JSONArray convert(Json source) {
		return JSONUtil.parseArray(source.asString());
	}
}

最终的代码非常简单,但是却踩了不少坑。

注意下面的配置类用了就会报错,不做参考,仅做警醒

①错误的替换了所有的转换器


@EnableR2dbcRepositories(basePackages = "com.yupi.yunpicturebackend.repositories")//开启R2DBC的仓库功能
@Configuration
public class R2DBCConfig {
	@Bean
	public R2dbcCustomConversions customConversions() {
		return new R2dbcCustomConversions(Collections.singletonList(new PictureReadingConvert()));
	}
	
}

这里直接调用了转换器的new方法,在不熟悉R2dbcCustomConversions源码的情况下忽略了static静态块的资源预热,导致报错

org.springframework.dao.InvalidDataAccessApiUsageException: Nested entities are not supported

缺少基本的类型处理。

②基本类型的转换器错误

@EnableR2dbcRepositories(basePackages = "com.yupi.yunpicturebackend.repositories")//开启R2DBC的仓库功能
@Configuration
public class R2DBCConfig {
	
	@Bean
	public R2dbcCustomConversions customConversions() {
		List<Object> converters = new ArrayList();
		converters.add(new JsonArrayToJson());
		converters.add(new JsonToJsonArray());
		return R2dbcCustomConversions.of(
				MySqlDialect.INSTANCE,
				converters
		);
	}
}

当时这段代码的主体都是粘过来的,乍一看没有毛病,一运行就报错。

仍旧是缺少类型处理的提示,此时我还以为已经将PostgreSQL的驱动给补上了,于是开始查看源代码。

是的源代码对于第一个对象取走了一个东西,英语底子不好,于是我继续追踪

 它拿走了一个SimpleTypeHolder对象,此时我还没有意识到它是做什么的,于是开始查看R2dbcCustomConversions构造函数的第二个参数以及storeConverters。其实这里就可以猜出这里的问题是用错了转化的支持对象,当然后面的探索更加坚定了我的想法。

再往下深入就知道, R2dbcCustomConversions是根据第一个参数获取提前定义好的适配器转化器,因此如果这里使用的是postgresql应该写入postgresql的支持,而不是MySQL的。后面传递的Collection对象converters便是我们的自定义转化器。

当我们将这些都实现以后,R2dbcCustomConversions便会(本人感觉)根据支持的类型转换生成一个Set<Class<?>>用来进行数据加载时判断是否为系统支持的类型,如果支持再通过重载调用我们定义的converter转化器,因为不是这个项目的重点,因此也不再深究。

不过这种类型代理的思想非常值得学习,这也为我之前制作的协议适配器带来一定的影响,当时没有这种代理的思想,很多的协议报文解析都是按照字节长度逐步拆分,再进行基本类型转换的,按照这种思想我可以重新设计,如设计出协议头类型适配器,协议尾部检验方法适配器,协议主体适配器,以及重新设计,可按照数据库存储的数据进行动态处理的“基本类型”适配器,这样差不多直接把项目重新写一遍了,本篇的目的便是总结经验,方便我学习完鱼图图项目后重新改良协议适配器做的总结。

为什么说R2dbcCustomConversions会根据支持的类型转换生成一个Set<Class<?>>

当然这也是本人的一个猜测,它更深层次的底层我是没有能力和时间进行钻研的,我们来看一下这种写法。

@ReadingConverter
public class JsonToJsonArray
		implements Converter<Json, JSONArray> {
	@Override
	public JSONArray convert(Json source) {
		return JSONUtil.parseArray(source.asString());
	}
}
@EnableR2dbcRepositories(basePackages = "com.yupi.yunpicturebackend.repositories")//开启R2DBC的仓库功能
@Configuration
public class R2DBCConfig {
	
	@Bean
	public R2dbcCustomConversions customConversions() {
		List<Object> converters = new ArrayList();
		converters.add((Converter<Json, JSONArray>) source -> JSONUtil.parseArray(source.toString()));
		converters.add(new JsonArrayToJson());
		converters.add(new JsonToJsonArray());
		return R2dbcCustomConversions.of(
				PostgresDialect.INSTANCE,
				converters
		);
	}
}

我们可以看到JsonToJsonArray对象完全可以使用lambda表达式来进行代替,那我们注释掉JsonArrayToJson跑一下程序。

通过对比可以发现,spring底层确实尝试获取 converters 字段的类型,并且当我们切换成new对象的版本也成功进行了数据的保存。

当然以上只是本人通过实践研究出来的一种解决思路,本人能力有限可能有一些底层的实现并不能很好的发现,如果你有什么新的看法,也欢迎进行评论指出问题,感谢您的合作


http://www.kler.cn/a/509960.html

相关文章:

  • MyBatis(六)关联查询
  • 算法与数据结构——复杂度
  • 《计算机网络》课后探研题书面报告_网际校验和算法
  • Nmap之企业漏洞扫描(Enterprise Vulnerability Scanning for Nmap)
  • 浅谈云计算21 | Docker容器技术
  • 【NextJS】PostgreSQL 遇上 Prisma ORM
  • Leetcode - 周赛432
  • leetcode34-排序数组中查找数组的第一个和最后一个位置
  • Learning Prompt
  • Kubernetes (K8s) 权限管理指南
  • 【Linux】15.Linux进程概念(4)
  • linux 安装jdk1.8
  • 【脑机接口数据处理】bdf文件转化mat文件
  • AI Prompt 设计指南:从基础构建到高质量生成的全面解析
  • h5使用video播放时关掉vant弹窗视频声音还在后台播放
  • Centos7将/dev/mapper/centos-home磁盘空间转移到/dev/mapper/centos-root
  • 分布式CAP理论介绍
  • Dart语言
  • 计算机视觉语义分割——U-Net(Convolutional Networks for Biomedical Image Segmentation)
  • 【视觉惯性SLAM:十六、 ORB-SLAM3 中的多地图系统】
  • 深入探索Go语言中的临时对象池:sync.Pool
  • Vue2.0的安装
  • K210视觉识别模块
  • 向harbor中上传镜像(向harbor上传image)
  • 模块化架构与微服务架构,哪种更适合桌面软件开发?
  • 【Unity】使用UniRx来快速完成Unity中的信号层开发工作。