IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 利用Jdk反射机制和适配器模式让Spring Data Jpa兼容各种数据库 -> 正文阅读

[Java知识库]利用Jdk反射机制和适配器模式让Spring Data Jpa兼容各种数据库

简介

因项目需求需要在应用编译发布后不改源代码的方式下支持MySql,Oracle等国产数据库,
因各数据库厂商的sql函数,字段类型,主键自增策略有差异,故本人基于jdk反射机制+适配器模式实现该需求。
本文以MySql,Oracle为例,下表列举了一些简单的差异信息

数据库文本块类型日期格式化函数主键自增
mysqltextdate_format()支持GenerationType.IDENTITY
oracleclobto_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数据库启动测试类,控制台输出信息如下
在这里插入图片描述

4.项目配套代码下载

https://github.com/Dominick-Li/jpa-dbadapter
创作不易,要是觉得我写的对你有点帮助的话,麻烦在github上帮我点下 Star

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 12:26:14  更:2022-05-09 12:28:07 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:33:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码