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

记录一次跨库连表的坑

一、背景

1. 业务背景

一个微服务项目,本次业务主要涉及两个板块,分别是 文章管理 和 系统管理。具有开发环境、测试环境、生产环境三个环境。其中,开发环境和测试环境用的是同一个服务器(nacos和MySQL都是用的同一个服务器中的)。

文章管理模块是系统的业务模块之一,放在article_server微服务中。在开发环境中的相关数据表放在article_server_dev数据库中,在测试环境中的相关数据表放在article_server_test数据库中。
系统管理是另外一个微服务system_manage中,包括用户管理、角色管理、组织管理等功能。在开发环境中系统管理的相关数据表放在system_manage_dev数据库中,在测试环境中系统管理的相关数据表放在system_manage_test数据库中。

具体数据库信息如下
在这里插入图片描述

此时有一个界面的需求是展示文章信息的列表,同时展示订单信息的创建人相关信息(创建人姓名、工号、电话号码)。由于是业务模块,所以在article_server微服务中编写业务逻辑代码,文章相关信息在文章数据库article_server_dev和article_server_test中的article表中;而要同时带出的文章编辑用户信息,则存储在system_manage_dev和system_manage_test数据库中的sys_user表中。在article表中有一个create_user_id字段,存放着与用户的关联字段。

2. 踩坑时期的做法

在一开始做这个业务的时候,偷了个懒,直接在mapper层使用了跨库联表,本地自测一切正常,发版到开发环境后,前端界面一切正常。具体如下:
Controller层

    @Resource
    private ApArticleService apArticleService;
    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    @PostMapping(name = "获取文章信息",value = "/selectArticleDetail")
    @ApiOperation("获取文章信息")
    public ResponseResult selectArticleDetail(@RequestBody ArticleInfoDto req){
        if (Objects.isNull(req) || StringUtils.isBlank(req.getArticleId())) {
            return ResponseResult.errorResult(400, "参数有误");
        }
        ArticleDto articleDto = apArticleService.selectArticleDetail(req);
        return ResponseResult.okResult(articleDto);
    }

Service层接口

    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    ArticleDto selectArticleDetail(ArticleInfoDto dto);

Service层实现类

    @Resource
    private ApArticleMapper apArticleMapper;
    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    @Override
    public ArticleDto selectArticleDetail(ArticleInfoDto dto) {
        return apArticleMapper.selectArticleDetail(dto.getArticleId());
    }

Mapper层接口

    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    ArticleDto selectArticleDetail(String articleId);

Mapper层xml文件

    <select id="selectArticleDetail" resultMap="resultMap">
        SELECT
        aa.*, su.real_name AS realName, su.phone, su.employ_no AS employNo
        FROM `ap_article` aa
        LEFT JOIN sys_user su ON aa.create_user_id = su.id
        <where>
            and aa.is_delete != 1
            and su.is_delete != 1
            <if test="articleId != null and articleId != ''">
                and aa.id = #{articleId}
            </if>
        </where>
    </select>

二、踩坑问题与原因排查

1. 踩坑问题

此时开发环境一切正常,发版到测试环境之后,发现测试环境有几篇文章的所有用户信息全部消失不见。

2. 原因排查

排查后发现,测试环境环境所有消失不见的用户信息,都仅限于某两个用户 张三 和 李四 。其他用户发表的文章都可以正常发现。

到数据库中排查这两个用户的文章信息,发现根据article_server_test数据库中article表,查找一篇张三写的文章的记录,该记录 create_user_id 为 “c01d7916f74811efb5c78c8caa27867d”。以其为主键id,可以在system_manage_test数据库中的sys_user 表中的 id 匹配到 张三 的用户信息。

既然可以找到信息,那为什么没有查询到结果呢?

只有张三和李四这两个用户匹配不上用户信息,而其他用户可以匹配上,于是开始寻找这两个用户与其他用户的区别。比较后发现,其他用户的数据都是直接从开发数据库迁移过来的,所以开发数据库和测试数据库的主键Id相同。而这两个用户开发数据库和测试数据库的主键Id不同,开发环境system_manage_dev数据库的sys_user表,张三 的id为“f380dcc6f74811efb5c78c8caa27867d”,测试环境system_manage_test数据库的sys_user表,张三 的id为之间提到的“c01d7916f74811efb5c78c8caa27867d”。

开发环境两张表示意图:
在这里插入图片描述
在这里插入图片描述
测试环境两张表示意图:
在这里插入图片描述
在这里插入图片描述

排查到这里就想到了之前的跨库联表操作,意识到有可能是跨库联表后,导致测试环境的情况下,连的是system_manage_dev的sys_user,而不是system_manage_test的sys_user表。

为验证此猜想,手动暂时先将article_server_test数据库中article表中对应记录的create_user_id改为system_manage_dev的sys_user中 张三 的主键id,发现可以查到相应数据。结论得到验证。

三、解决方案

1. 在sql语句中将数据库名写死(不可行)

首先想到在sql语句中将数据库名写死,但是不可行,因为开发环境、测试环境数据库名都有细微的差别,采用此方案会导致每个环境的这段sql语句不一致,所以此方案不可行。

2. 使用Spring Boot的配置属性动态注入数据库名,实现环境隔离(可行)

在开发环境和测试环境的配置文件application-dev.yml和application-test.yml分别进行如下配置:

# application-dev.properties
system.manage.db=system_manage_dev
# application-test.properties
system.manage.db=system_manage_test

此时mapper层的连表语句如下:

    <select id="selectArticleDetail" resultMap="resultMap">
        SELECT
        aa.*, su.real_name AS realName, su.phone, su.employ_no AS employNo
        FROM `ap_article` aa
        LEFT JOIN ${system.manage.db}.sys_user su ON aa.create_user_id = su.id
        <where>
            and aa.is_delete != 1
            and su.is_delete != 1
            <if test="articleId != null and articleId != ''">
                and aa.id = #{articleId}
            </if>
        </where>
    </select>

3. 远程Feign调用(可行)

在system_manage微服务写一个根据用户Id获取用户信息的方法,并用Feign远程调用。代码如下:

远程Feign调用:

    @GetMapping("/api/sys/user/getUserById")
    ResponseResult getUserById(@PathVariable("userId") String userId);

Service层实现类改为:

    @Resource
    private ApArticleMapper apArticleMapper;
    @Resource
    private IArticleClient articleClient;
    /**
     * 获取文章信息
     *
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v2.0
     */
    @Override
    public ArticleDto selectArticleDetail(ArticleInfoDto dto) {
        ArticleDto articleDto = apArticleMapper.selectArticleDetail(dto.getArticleId());
        ResponseResult responseResult = articleClient.getUserById(articleDto.getCreateUserId());
        if (Objects.nonNull(responseResult) && Objects.nonNull(responseResult.getData()) ) {
            SysUser user = responseResult.getData();
            articleDto.setCreateUserName(user.getRealName);
            articleDto.setCreateUserNo(user.getEmployNo);
            articleDto.setPhone(user.getPhone);
        }
        return articleDto;
    }

Mapper层xml文件改为:

    <select id="selectArticleDetail" resultMap="resultMap">
        SELECT * FROM `ap_article`
        <where>
            and is_delete != 1
            <if test="articleId != null and articleId != ''">
                and id = #{articleId}
            </if>
        </where>
    </select>

同时在system_manager微服务中创建根据用户id获取用户信息的接口。
Controller层

    @Resource
    private SysUserService sysUserService ;
    /**
     * 获取用户信息
     * 
     * @param req 用户Id
     * @return 用户信息
     * @author admin
     * @since v2.0
     */
    @PostMapping(name = "获取用户信息",value = "/getUserById")
    @ApiOperation("获取用户信息")
    public ResponseResult getUserById(@RequestParam("userId") String userId){
        if (StringUtils.isBlank(userId)) {
            return ResponseResult.errorResult(400, "参数有误");
        }        
        SysUser user = apArticleService.getById(userId);
        return ResponseResult.okResult(user);
    }

四、原因分析

1.为什么这两个用户的主键id变了?

这两个用户是产品经理的个人账号,虽然也是从开发数据库迁移过来的,但是迁移后,产品经理在分配权限的时候把这两个账号先删除,再新建的,导致重新生成了uuid,所以主键id变了。

2.为什么跨库连表出现了问题?

首先,问题出现在连表时连错数据库了。

理想状态为:在开发环境是article_server_dev数据库的aritcle表和system_manage_dev数据库的sys_user表。在测试环境是article_server_test数据库的aritcle表和system_manage_test数据库的sys_user表。
能查到用户数据:
在这里插入图片描述

事实为:在开发环境是article_server_dev数据库的aritcle表和system_manage_dev数据库的sys_user表。在测试环境是article_server_test数据库的aritcle表和system_manage_dev数据库的sys_user表。

不能查到用户数据:
在这里插入图片描述

其次,思考为什么会出现这种状态。在article_server微服务中,开发环境配置的数据源为article_server_dev数据库,测试环境配置的数据源为article_server_test数据库。在system_manage微服务中,开发环境配置的数据源为system_manage_dev数据库,测试环境配置的数据源为system_manage_test数据库。
我在mapper层的xml文件中连表查询的时候,没有指定sys_user所在的数据库。这时候SpringBoot会做什么?

Spring Boot通过Profile(如dev、test)管理不同环境的配置。每个微服务的数据源(如article_server的datasource.url)会正确指向对应环境的数据库(如article_server_test)。
尽管article_server和system_manage各自的数据源配置正确,但联表查询涉及跨库时,需显式指定目标数据库名。若未通过配置动态注入数据库名,数据库会在当前连接的库中查找表,而sys_user表并不在article_server_test数据库中,所以SQL会固定指向某个环境(如system_manage_dev),导致测试环境错误。

五、最终办法

1. 配置文件动态配置数据库名。

2. 放弃跨库联表,分别在两微服务查询并使用Feign调用。


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

相关文章:

  • 算法刷题-2025年03月01日
  • Python使用pyobdc库和tkinter框架连接数据库
  • 蓝桥备赛(七)- 函数与递归(中)
  • 深度学习-12.变换器(Transformer)
  • 【Uniapp-Vue3】使用uniCloud.uploadFile上传图片到云存储
  • 青少年编程与数学 02-010 C++程序设计基础 13课题、数据类型
  • 九牧的“AI梦想曲”:卫浴场景进入到机器人时代
  • 【Java 后端】Restful API 接口
  • 高级算法分析与设计-分治法
  • 一个py文件搞定mysql查询+Json转换+表数据提取+根据数据条件生成excel文件+打包运行一条龙
  • Spring MVC框架六:Ajax技术
  • Redis面试常见问题——使用场景问题
  • React Portals深度解析:突破组件层级的渲染艺术
  • spring boot打包插件的问题
  • 【Mac】git使用再学习
  • Django应用的高级配置和管理
  • 【Python 数据结构 3.顺序表】
  • python | 2 个删除列表中空字符串元素的方法
  • 【GenBI优化】提升text2sql准确率:建议使用推理大模型,增加重试
  • The First项目报告:VANA如何重塑数据所有权与AI训练