文章目录
简介
因项目需求需要在应用编译发布后不改源代码的方式下支持MySql,Oracle等国产数据库,
因各数据库厂商的sql函数,字段类型,主键自增策略有差异,故本人基于jdk反射机制+适配器模式实现该需求。
本文以MySql,Oracle为例,下表列举了一些简单的差异信息
数据库 | 文本块类型 | 日期格式化函数 | 主键自增 |
---|---|---|---|
mysql | text | date_format() | 支持GenerationType.IDENTITY |
oracle | clob | to_char() | 不支持GenerationType.IDENTITY,支持GenerationType.SEQUENCE |
jpa默认String类型映射到数据库是字符串(varchar)类型,如果需要存储文本块字段,需要通过@Column注解的columnDefinition属性指定类型为text或者clob,如果项目默认在mysql环境开发的则切换到oracle环境则需要修改注解里的属性值,这种改动特别低效,我们可以通过Jdk反射加自定义注解解决这个问题。
@Column(columnDefinition = "text")
private String likeBookList;
1.依赖及配置文件
pom文件如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--mysql jdbc连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--oracle jdbc连接驱动 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>12.2.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
application.yml文件如下
下面通过spring.profiles.active属性选择使用mysql或者oracle数据库
#------------- 公共的配置属性 ---------------
spring:
profiles:
#使用mysql或oracle改下面属性即可
active: mysql
datasource:
type: com.zaxxer.hikari.HikariDataSource
hikari:
# 连接池名称
pool-name: MyHikariCP
#最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
minimum-idle: 10
#连接池最大连接数,默认是10 (cpu核数量 * 2 + 硬盘数量)
maximum-pool-size: 30
#空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
idle-timeout: 600000
#连接最大存活时间,不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
max-lifetime: 1800000
#连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒
connection-timeout: 30000
jpa:
show-sql: true
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
ddl-auto: update
properties:
hibernate:
jdbc:
enable_lazy_load_no_trans: true
#------------- mysql 配置 ---------------
---
spring:
config:
activate:
on-profile: mysql
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_adapter?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=GMT%2b8&characterEncoding=utf8&connectTimeout=1000&socketTimeout=15000&autoReconnect=true&cachePrepStmts=true&useServerPrepStmts=true
username: root
password: 123456
hikari:
#用于测试连接是否可用的查询语句
connection-test-query: SELECT 1
jpa:
database: mysql
hibernate:
properties:
hibernate:
jdbc:
#配置hibernate方言使用Mysql
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
#------------- oracle 配置 ---------------
---
spring:
config:
activate:
on-profile: oracle
datasource:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@localhost:1521:ORCL
username: DBADAPTER
password: 123456
hikari:
#用于测试连接是否可用的查询语句
connection-test-query: SELECT * from dual
jpa:
database: oracle
properties:
hibernate:
jdbc:
#配置hibernate方言使用Oracle
dialect: org.hibernate.dialect.OracleDialect
#------------- 可以再扩展支持hibernate方言的数据库 ---------------
2.功能实现代码
2.1.自定义字段类型注解
当使用oracle数据库的时候,使用注解内的value属性替换类中的@Column注解columnDefinition属性,使用注解内strategy替换@GeneratedValue注解里的strategy属性。
/**
* @Author Dominick Li
* @CreateTime 2022/3/11 10:57
**/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OracleCloumnDefinition {
/**
* 数据库字段类型
*/
String value() default "" ;
/**
* 主键自增策略
*/
GenerationType strategy() default GenerationType.SEQUENCE;
}
2.2.添加适配器
枚举出应用支持的数据库
/**
* @Description 目前支持的数据库类型
* @Author Dominick Li
* @CreateTime 2022/3/9 17:15
**/
public enum DataBaseType {
MYSQL("mysql"),
ORACLE("oracle"),
;
private String name;
DataBaseType(String name) {
this.name = name;
}
public static DataBaseType nameOf(String name) {
for (DataBaseType dataBaseType : DataBaseType.values()) {
if (dataBaseType.name.equals(name)) {
return dataBaseType;
}
}
return null;
}
}
2.2.1.定义适配器抽象类
@Slf4j
public abstract class DataBaseNativeSql {
@Value("${spring.profiles.active}")
private String activeDatabase;
private static DataBaseType dataBaseType;
public static DataBaseType getDataBaseType() {
return dataBaseType;
}
/**
* 把日期字段格式化成只包含年的字符 例如:2021-12-24 10:20 返回2021
*/
public abstract String format_year();
/**
* 把日期字段格式化成只包含年月的字符 例如:2021-12-24 10:20 返回2021-12
*/
public abstract String format_year_month();
/**
* 把日期字段格式化成只包含年月日的字符 例如:2021-12-24 10:20 返回2021-12-24
*/
public abstract String format_year_month_day();
@PostConstruct
public void supportsAdvice() {
log.info("当前系统使用的数据库是{}", activeDatabase);
dataBaseType = DataBaseType.nameOf(activeDatabase);
if (dataBaseType == null) {
log.error("未适配的数据库类型:{} ,系统异常退出!", activeDatabase);
throw new RuntimeException("未适配的数据库类型,系统异常退出!");
}
}
}
2.2.2.数据库适配器实现类
下面通过@ConditionalOnProperty配置只由当配置文件中的spring.profiles.active属性和当前注解里havingValue 值一致的时候,当前类才由Spring ioc管理,故DataBaseNativeSql抽象类的实例永远只由一个存在。
@Configuration
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "mysql")
public class MysqlNativeSqlAdaptation extends DataBaseNativeSql {
@Override
public String format_year() {
return "date_format(${field},'%Y')";
}
@Override
public String format_year_month() {
return "date_format(${field},'%Y-%m')";
}
@Override
public String format_year_month_day() {
return "date_format(${field},'%Y-%m-%d')";
}
}
@Configuration
@ConditionalOnProperty(name = "spring.profiles.active", havingValue = "oracle")
public class OracleNativeSqlAdaptation extends DataBaseNativeSql {
@Override
public String format_year() {
return "to_char(${field},'yyyy')";
}
@Override
public String format_year_month() {
return "to_char(${field},'yyyy-mm')";
}
@Override
public String format_year_month_day() {
return "to_char(${field},'yyyy-mm-dd')";
}
}
2.3.添加测试用得模型类
默认使用的mysql的字段类型,下面使用到OracleCloumnDefinition注解来支持动态修改字段类型
**/
@Data
@Entity
@Table(name = "sys_user")
public class SysUser {
/**
* 主键 自增策略 oracle=GenerationType.SEQUENCE, mysql=GenerationType.IDENTITY, 如果Id使用雪花算法生成或者UUID则可设置为默认值GenerationType.AUTO
* mysql数据库只有int类型支持主键自增,Long类型默认对应的Mysql数据库的bigint类型不支持自增
*/
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
@Column(columnDefinition = "int")
@OracleCloumnDefinition("number")
private Long id;
/**
* 用户名
*/
private String username;
/**
* 喜欢看的书 字段类型设置,默认用Mysql数据库配置为文本块text类型存储,oracle的文本块用clob存储
* 如果需要扩展其它的数据库,可根据Column配置的columnDefinition类型是否兼容,不兼容需要自定义扩展和@OracleCloumnDefinition类似的注解
*/
@OracleCloumnDefinition("clob")
@Column(columnDefinition = "text")
private String likeBookList;
/**
* 创建时间
*/
private Date createTime;
/**
* 是否可用
* mysql中Column默认使用的bit类型存储boolean类型的值,oracle默认不支持boolean,需要动态修改columnDefinition成number类型
*/
@OracleCloumnDefinition("number")
@Column
private boolean enabled;
}
public interface SysUserRepository extends JpaRepository<SysUser,Integer> {
List<SysUser> findAllByEnabled(boolean enabled);
}
2.4.配置需要通过反射修改字段类型类文件路径
public class WriteClassNameToFileUtils {
public static void main(String[] args) throws Exception {
List<String> packNameList = Arrays.asList("com.ljm.dbadapter.model" );
StringBuilder sb=new StringBuilder();
for (String packageName : packNameList) {
String path = packageName.replaceAll("\\.", "/");
File dir = org.springframework.util.ResourceUtils.getFile("classpath:" + path);
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
continue;
} else {
if(sb.length()!=0){
sb.append("\n");
}
String className = packageName + "." + file.getName().replace(".class", "");
sb.append(className);
}
}
}
System.out.println(sb.toString());
String resourcePath= new File("src/main/resources/").getAbsolutePath();
File file=new File(resourcePath+ File.separator+"className.txt");
FileOutputStream fileOutputStream=new FileOutputStream(file);
fileOutputStream.write(sb.toString().getBytes());
fileOutputStream.flush();
fileOutputStream.close();
}
}
执行完会在项目的resources目录下生成这个文件
文件内容如下
com.ljm.dbadapter.model.SysUser
2.5.通过反射修改字段对应的数据库字段类型 (核心代码)
@Slf4j
public class ColumnDefinitionAdaptaion {
public void init() {
try {
//只有oracle需要对字段特殊处理,其它的按照默认类型即可,如需要扩充其它数据库加上 || 判断条件即可
if (DataBaseNativeSql.getDataBaseType() == DataBaseType.ORACLE) {
log.info("*****************************对实体类字段进行自动适配开始******************************");
//第一步 加载需要扫描的class文件
List<Class<?>> classList = getClassList();
if (classList == null) {
return;
}
//第二步 通过反射机制修改类的Cloumn的columnDefinition属性
for (Class<?> clazz : classList) {
modifyCloumnDefinition(clazz);
}
log.info("*****************************对实体类字段进行自动适配结束******************************");
}
} catch (Exception e) {
e.printStackTrace();
log.error("ColumnDefinitionAdaptaion error:{}", e.getMessage());
}
}
/**
* 读取className文件获取需要加载的class文件
*/
private List<Class<?>> getClassList() {
try (InputStreamReader isr = new InputStreamReader(getClass().getResourceAsStream("/className.txt"), "UTF-8")) {
List<Class<?>> classList = new ArrayList<>();
Class<?> beanClass;
BufferedReader br = new BufferedReader(isr);
String className = "";
while ((className = br.readLine()) != null) {
beanClass = Class.forName(className);
//只加载包含@Table注解的类
if (beanClass.isAnnotationPresent(Table.class)) {
classList.add(beanClass);
}
}
return classList;
} catch (Exception e) {
log.error("getClass error:{}", e.getMessage());
return null;
}
}
/**
* 修改注解里的字段类型
*/
public static void modifyCloumnDefinition(Class<?> clas) throws Exception {
DataBaseType dataBaseType = DataBaseNativeSql.getDataBaseType();
Column column;
GeneratedValue generatedValue;
OracleCloumnDefinition oracleCloumnDefinition;
Map generatedValueMemberValues;
boolean modify;
InvocationHandler invocationHandler;
Field hField;
Field[] fields = clas.getDeclaredFields();
for (Field field : fields) {
generatedValueMemberValues = null;
modify = false;
if (field.isAnnotationPresent(Column.class)) {
if (dataBaseType == DataBaseType.ORACLE && field.isAnnotationPresent(OracleCloumnDefinition.class)) {
modify = true;
} else if (true) {
//可以在if逻辑处扩展其它数据库判断逻辑
}
}
if (modify) {
column = field.getAnnotation(Column.class);
// 获取column这个代理实例所持有的 InvocationHandler
invocationHandler = Proxy.getInvocationHandler(column);
// 获取 AnnotationInvocationHandler 的 memberValues 字段
hField = invocationHandler.getClass().getDeclaredField("memberValues");
// 因为这个字段事 private修饰,所以要打开访问权限
hField.setAccessible(true);
// 获取 memberValues
Map memberValues = (Map) hField.get(invocationHandler);
//判断是否为主键并设置了自增策略
if (field.isAnnotationPresent(Id.class) && field.isAnnotationPresent(GeneratedValue.class)) {
//修改自增策略
generatedValue = field.getAnnotation(GeneratedValue.class);
invocationHandler = Proxy.getInvocationHandler(generatedValue);
hField = invocationHandler.getClass().getDeclaredField("memberValues");
hField.setAccessible(true);
generatedValueMemberValues = (Map) hField.get(invocationHandler);
}
// 修改 value 属性值
if (dataBaseType == DataBaseType.ORACLE) {
oracleCloumnDefinition = field.getAnnotation(OracleCloumnDefinition.class);
memberValues.put("columnDefinition", oracleCloumnDefinition.value());
if (generatedValueMemberValues != null) {
//修改主键的自增策略
generatedValueMemberValues.put("strategy", oracleCloumnDefinition.strategy());
log.info("字段名称:{},需要注解自增策略为为:{}", field.getName(), oracleCloumnDefinition.strategy());
}
} else if (true) {
//可以在if逻辑处扩展其它数据库需要修改的字段类型注解
}
log.info("字段名称:{},需要修改类型为:{}", field.getName(), column.columnDefinition());
}
}
}
}
2.6.在数据库连接源加载之前调用2.5的工具类
添加配置类获取配置文件里面的属性
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DataBaseConfig {
private String url;
private String username;
private String password;
private String driverClassName;
}
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public class HikariPoolConfig {
private String poolName;
private int minimumIdle;
private int maximumPoolSize;
private long idleTimeout;
private long maxLifetime;
private long connectionTimeout;
private String connectionTestQuery;
}
在数据库连接源加载之前初始化字段适配器类
@Slf4j
@Configuration
public class InitConfig {
@Autowired
private DataBaseConfig dataBaseConfig;
@Autowired
private HikariPoolConfig HikariPoolConfig;
@Resource
private DataBaseNativeSql dataBaseNativeSql;
@Bean
@Primary
public DataSource datasource() {
log.info("数据库连接池加载前配置....");
//初始化字段适配器
new ColumnDefinitionAdaptaion().init();
HikariDataSource dataSource = new HikariDataSource();
//克隆配置属性到到HikariDataSource实例中
BeanUtils.copyProperties(HikariPoolConfig, dataSource);
dataSource.setJdbcUrl(dataBaseConfig.getUrl());
dataSource.setUsername(dataBaseConfig.getUsername());
dataSource.setPassword(dataBaseConfig.getPassword());
dataSource.setDriverClassName(dataBaseConfig.getDriverClassName());
return dataSource;
}
}
3.测试
3.1.测试数据库字段适配
测试代码如下.
@Autowired
private SysUserRepository sysUserRepository;
@Test
void contextLoads() {
SysUser sysUser = new SysUser();
sysUser.setCreateTime(new Date());
sysUser.setUsername("张三");
sysUser.setEnabled(false);
sysUserRepository.save(sysUser);
sysUser = new SysUser();
sysUser.setCreateTime(new Date());
sysUser.setUsername("李四");
sysUser.setEnabled(true);
sysUserRepository.save(sysUser);
List<SysUser> sysUserList = sysUserRepository.findAllByEnabled(false);
System.out.println(sysUserList.size());
}
默认使用mysql数据库测试,结果如下表示默认应用是正常启动的
切换到oracle数据库配置然后测试,修改配置文件属性spring.profiles.active为oracle
启动测试类可以看到下面打印了修改字段类型的信息,最下面插入数据到数据库中成功了。
3.2.测试数据库函数适配
@SpringBootTest
class DbAdapterApplicationTests {
@Autowired
private SysUserRepository sysUserRepository;
private EntityManagerFactory emf;
@Resource
private DataBaseNativeSql dataBaseNativeSql;
@PersistenceUnit
public void setEntityManagerFactory(EntityManagerFactory emf) {
this.emf = emf;
}
@Test
void contextLoads() {
EntityManager em = emf.createEntityManager();
//1=根据年分组,2=根据年月分组,3=根据年月日分组
Integer groupType = 1;
try {
Query query;
String groupBy;
if (groupType == 1) {
//年分组
groupBy = dataBaseNativeSql.format_year().replace("${field}", "createTime");
} else if (groupType == 2) {
//月分组
groupBy = dataBaseNativeSql.format_year_month().replace("${field}", "createTime");
} else {
//按日如果没传时间参数,默认查最近一个月的
groupBy = dataBaseNativeSql.format_year_month_day().replace("${field}", "createTime");
}
//拼接要执行的sql语句
StringBuilder sql = new StringBuilder("select ");
sql.append(groupBy);
sql.append(" gb,count(id) from sys_user group by ");
sql.append(groupBy);
//创建query对象
query = em.createNativeQuery(sql.toString());
List<Object[]> lists = query.getResultList();
for (Object[] data : lists) {
System.out.println(data[0] + "注册的用户数为=" + data[1]);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (em != null) {
em.close();
}
}
}
}
使用mysql数据库启动测试类,控制台输出信息如下
使用oracle数据库启动测试类,控制台输出信息如下