前言
经过初步方案的总结分析,我们不但发现,初步方案不仅重复代码太多,而且调用dao层接口不够灵活,本文从这两个缺点入手,并提出解决思路及验证过程,并形成升级方案。
一、思路分析
1、service层升级思路分析
- 从初步方案中对service层的改造我们不难发现,工作内容其实很简单:添加一个dao层接口对象,根据数据库驱动类型选择适用的dao层接口对象调用接口
- 虽然工作内容简单,但是工作量却不少
- 在service层添加新dao层接口对象的原因是新、老dao层接口没有关系,能不能让他们产生关系呢,最简单且直观的方式就是让新dao层接口继承老dao层接口
- 新dao层接口继承了老dao层接口,这样新dao层接口中无需添加任何接口,有变化的时候在老dao层接口中实现即可,新dao层接口完全继承
- 这样,新dao层接口就成了子dao层接口,老dao层接口就成了父dao层接口,一种数据库对应一个子dao层接口,而且子dao层接口无需添加任何方法,直接从父dao层接口中获取。看起来还不错
- 还有一个问题就是service层调用问题,不改动service层时,子dao层接口调用的还是父dao层接口的实现
- 为什么还是调用父dao层接口的实现呢,这就牵涉到@Autowire的注入机制:先查看接口有几个实现类,如果只有一个实现类,就将该实现类注入;如果有多个实现类时,就使用和变量名称相同的bean对象注入,除非手动指定;因为,父dao层接口有多个实现,而service层定义的变量名称和父接口的实现bean对象名称一致,所以默认是父dao层接口的实现
- 除非手动指定给我们提供了解决思路:怎么样手动指定呢?两种方式:第一种是在变量上使用@Qualifier(“bean名称”)注解手动指定;第二种是在某一个实现类上使用@Primary注解,只能有一个
- 看起来在所有子dao层接口上使用优先注入@Primary注解再配合条件注解@ConditionalOnProperty(name = “spring.datasource.driver-class-name”,havingValue = “数据库驱动类”)不错:在Spring容器中只注册当前数据库适配的子dao层接口,加载了该子dao层接口后就会优先注入,又不会和其他@Primary冲突
- 遗憾的是@Primary和@ConditionalOnProperty对mapper接口不生效!?!?
- 看起来只能使用@Qualifier(“bean名称”)的方式,但是bean名称需要动态变化,而bean名称只能使用硬编码或者常量
- 这样看起来在只能在每个service层添加一个需要注入的dao层接口对象变量名称的常量,@Qualifier(常量)这种方式了
- 为了减少修改的工作量,约定子Mapper接口按照:业务+数据库类型+Mapper方式命名,示例:userPgMapper,再将数据库类型提出一个公用的常量,这样在修改数据库类型时只修改这一个公用的常量,虽然做不到一套代码适配所有数据库,但是看起来修改的工作量还是可以接受的。那就暂时采用这种方案。
2、dao层升级思路分析
- 从初步方案中对dao层的改造,我们不难发现,mapper.xml中的内容不管SQL是否相同,都需要重写一份。而且mapper.java除了名称内容完全一致
- 从service层升级思路分析中我们改造了子dao层接口,让每个数据库对应的dao层接口继承原始的dao层接口
- 这样做的好处不仅仅是对子dao层接口产生了积极的影响,而且意外对mapper.xml也产生了重大影响:父dao层接口实现的方法,子dao层接口可以直接使用。
- 换句话说就是父dao层接口的实现mapper.xml中的增删改查语句,子dao层接口的mapper.xml无需重复实现!!!
- xml之间不存在继承关系,为什么看起来像是子mapper.xml继承了父mapper.xml呢???
- 这是因为子dao层接口继承了父dao层接口,当调用子接口的某个方法时,子接口没有实现时就会调用父接口的实现,而父dao层接口的实现就是mapper.xml
- 这样就神奇的出现了子mapper.xml继承了父mapper.xml的错觉。
二、项目改造
1、项目改造
- 如果自己有项目,可以在自己项目上测试,如果没有或者不想在自己项目测试,可以使用同一套代码适配多种数据库之初步方案的示例代码。如果使用示例项目,先根据README.md正确运行示例项目。
- 修改Service层代码:使用@Qualifier(常量)指定Mapper接口实现的bean名称
package com.yu.service.impl;
import com.yu.dao.UserMapper;
import com.yu.entity.User;
import com.yu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
private static final String DATABASE_TYPE = "Pg";
private static final String MAPPER_NAME = "user" + DATABASE_TYPE + "Mapper";
@Autowired
@Qualifier(MAPPER_NAME)
private UserMapper userMapper;
@Override
public List<User> list(User user) {
return userMapper.list(user);
}
}
- 修改dao层代码:子接口继承父接口
package com.yu.dao;
public interface UserPgMapper extends UserMapper {
}
- 修改Mapper.xml:子mapper.xml中实现与父mapper.xml语法不一致的增删改查语句;因为这里只有一个查询语句,又与父mapper.xml的SQL语句有差异,所以看起来像是实现了全部语句,实际开发时没有差异的可以删除,需要改动时只改动父mapepr.xml并保证所有数据库适配即可,如果某个数据库的SQL不适配,在不适配数据库对应的子mapper.xml中添加需要适配的SQL,像下面的mapper.xml示例
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yu.dao.UserPgMapper">
<!--继承方式获取表与entity映射关系,方便本mapper.xml使用-->
<resultMap type="User" id="BaseResultMap" extends="com.yu.dao.UserMapper.BaseResultMap"/>
<sql id="Base_Column_List">
<!--也可以使用include方式包含父mapper.xml中的sql,方便本mapper.xml使用,因为SQL不通用,所以这里不能直接包含-->
<!--<include refid="com.yu.dao.UserMapper.Base_Column_List"/>-->
select
id,
name
<!--TODO 特别注意,系统也有user表,所以这里需要加上模式-->
from multi_database.user
</sql>
<select id="list" parameterType="User" resultMap="BaseResultMap">
<include refid="Base_Column_List"/>
<where>
<if test="name != null and name != ''">
and name like concat('%', #{name}, '%')
</if>
</where>
</select>
</mapper>
- 测试PostgreSQL方式
2、添加MySQL方式
- 修改为MySQL数据库连接配置
# 数据库配置 TODO 根据实际情况修改
database:
ip: xxx
#MySQL 3306,PostgreSQL:5432
port: 3306
database: demo
# postgres需要
schema: multi_database
username: xxx
password: xxx
# mysql: com.mysql.cj.jdbc.Driver ;postgres: org.postgresql.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
# mysql: jdbc:mysql://${database.ip}:${database.port}/${database.database}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
# postgres: jdbc:postgresql://${database.ip}:${database.port}/${database.database}?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&¤tSchema=${database.schema},sys_catalog
url: jdbc:mysql://${database.ip}:${database.port}/${database.database}?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
# 数据源配置
spring:
datasource:
# 数据源
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: ${database.driver-class-name}
url: ${database.url}
username: ${database.username}
password: ${database.password}
- 修改Service层代码:将数据库类型常量由Pg改为Mysql
package com.yu.service.impl;
import com.yu.dao.UserMapper;
import com.yu.entity.User;
import com.yu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
private static final String DATABASE_TYPE = "Mysql";
private static final String MAPPER_NAME = "user" + DATABASE_TYPE + "Mapper";
@Autowired
@Qualifier(MAPPER_NAME)
private UserMapper userMapper;
@Override
public List<User> list(User user) {
return userMapper.list(user);
}
}
-
添加MySQL对应的dao层接口
package com.yu.dao; public interface UserMysqlMapper extends UserMapper { }
-
添加MySQL对应dao层接口的实现UserMysqlMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yu.dao.UserMysqlMapper">
<!--继承方式获取表与entity映射关系,方便本mapper.xml使用-->
<resultMap type="User" id="BaseResultMap" extends="com.yu.dao.UserMapper.BaseResultMap"/>
<sql id="Base_Column_List">
<!--使用include方式包含父mapper.xml中的sql,方便本mapper.xml使用-->
<include refid="com.yu.dao.UserMapper.Base_Column_List"/>
</sql>
</mapper>
- 测试MySQL方式
三、方案总结
从上面修改情况来看,虽然和初步方案有巨大提升,但是还有些冗余的地方,比如说需要修改常量来实现注入不同的dao层实现类、需要添加好多空dao层接口。
咱们先总结该方案的添加一种新数据库时需要哪些操作及其操作必要性:
- 添加新数据库驱动包,无可厚非,没问题
- 添加新数据库配置,无可厚非,没问题
- 修改service层注入对象的数据库类型常量,看似问题不大,能改最好
- service层直接无需改动,数据库类型常量修改时隐式的修改了@Qualifier(常量),无直接改动
- dao层添加新的子接口,子接口继承父接口,按照命名规则命名,有点怪:每种数据库对应一个空dao层接口
- 添加dao层接口的实现mapper.xml,并实现与父mapper.xml中不一致的增删改查语句,无可厚非,没问题
虽然看起来是一个比较好的方案,但是还有优化空间,下文会持续优化并形成最终方案。