欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

浅谈JPA数据库方言适配

程序员文章站 2022-04-15 14:44:51
...

文章以Mysql、Oracle为例,其他类型的数据库还没试过,理论上其他也是类似的实现。

一、什么是方言?

对于方言,其实很简单,按我的理解,就是遵从原有一些语言的规则,再结合自身特点而衍生出来的语言,就叫做方言。对于关系型数据库,不同的数据库虽然都遵循着jdbc规范设计,但在一些细节上还是存在不同,比如某个定义,某个函数的实现,就像我们国家56个名族都说普通话,但每个名族说出来的普通话还是会有不同。

JPA封装了hibernate适配了市面上所有主流的关系型数据库,提供了统一、便捷的CRUD操作,QueryDsl则是配合JPA使用以支持复杂查询。而对于某一些比较特殊的功能,QueryDsl没有直接提供api,但我们可以自己手动适配数据库方言实现。

二、设计思想

从Mysql和Oracle来看,他们虽然语法大体类似,但也存在则一些细节的差异。比如分组函数,mysql有concat_group,而oracle可以通过to_char(wm_concat())、listagg等实现;又比如mysql提供了"&","|","^"三种位运算,而oracle在位运算上只提供了bitand()函数。对于这些差异,我们希望在上层代码的调用上能屏蔽掉,依赖QueryDsl和JPA适配数据库的方言就可以实现。

我们可以自定义封装一些函数,对外保持统一接口,通过区分不同的数据库环境,调用不同的函数模板,达到接口对上层调用透明的效果。

三、具体实现

3.1 元数据

定义一个对象JpaDatabase保存数据库适配的相关的一些元数据,主要包含当前的数据库类型,自定义的方言,同时提供初始化方法。

@Component
public class JpaDatabase {
    private Database database;
    private String dialect;
    /**
     * 初始化基础信息
     * @param database
     */
    public void init(Database database){
        initDialect(database);
    }
    /**
     * 初始化数据库方言
     */
    private void initDialect(Database database){
        switch (database){
            case MYSQL:
                initMysqlDatabase();
                break;
            case ORACLE:
                initOracleDatabase();
                break;
            case DB2:
                initDB2Database();
                break;
            default:
                break;
        }
    }
    private void initMysqlDatabase(){
        this.database = Database.MYSQL;
        this.dialect = "com.keduw.common.jpa.dialect.KdMysqlDialect";
    }
    private void initOracleDatabase(){
        this.database = Database.ORACLE;
        this.dialect = "com.keduw.common.jpa.dialect.KdOracleDialect";
    }
    private void initDB2Database(){
        this.database = Database.ORACLE;
    }
    public boolean isMysql(){
        return Database.MYSQL.equals(database);
    }
    public boolean isOracle(){
        return Database.ORACLE.equals(database);
    }
    
    /**省略get方法**/
}

初始化属性,在jpa初始化entityManager和jpaQueryFactory的同时初始化JpaDatabase属性,赋值当前环境的数据库类型和对应的方言。

/**
 * 数据源,通过数据源判断是当前是那种类型的数据库
 */
@Autowired
private DataSourceConfig sourceConfig;

@Autowired
private JpaDatabase jpaDatabase;

/**
 * 初始化数据库类型信息
 */
private void initDatabase(){
    String url = sourceConfig.getConfig().getUrl().toLowerCase();
    if(url.contains("mysql")){
        jpaDatabase.init(Database.MYSQL);
    }else if(url.contains("oracle")){
        jpaDatabase.init(Database.ORACLE);
    }else if(url.contains("db2")){
        jpaDatabase.init(Database.DB2);
    }else {
        throw new IllegalArgumentException("can not support "+ url);
    }
}
/**
 * 初始化entityManagerFactory
 * @return
*/
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
    LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
    /*省略一些代码*/
    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setGenerateDdl(false);
    //注入数据库类型和方言
    vendorAdapter.setDatabase(jpaDatabase.getDatabase());
    String dialect = jpaDatabase.getDialect();
    if(StringUtil.isNotBlank(dialect)){
        vendorAdapter.setDatabasePlatform(dialect);
    }
    factory.setJpaVendorAdapter(vendorAdapter);
    factory.setJpaDialect(new HibernateJpaDialect());
    /*省略一些代码*/
    return factory;
}
3.2 自定义mysql方言

SQLFunctionTemplate是hibernate提供的SQL函数模板定义方法,?1和?2代表传入的参数。registerFunction则表示将定义好的SQL模板注册到指定的函数声明上。

public class XwMysqlDialect extends MySQLDialect {
    public XwMysqlDialect(){
        super();
        registerFunction("bitand", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 & ?2)"));
        registerFunction("bitor", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 | ?2)"));
        registerFunction("bitxor", new SQLFunctionTemplate(IntegerType.INSTANCE, "(?1 ^ ?2)"));
        registerFunction("concat", new SQLFunctionTemplate(StringType.INSTANCE, "group_concat(?1 separator ',' )"));
    }
}
3.3 自定义oracle方言

也可以用StandardSQLFunction定义标准的函数模板。

public class XwOracleDialect extends Oracle12cDialect {
    public XwOracleDialect(){
        super();
        registerFunction("bitand", new StandardSQLFunction("bitand"));
        registerFunction("concat", new SQLFunctionTemplate(StringType.INSTANCE, "to_char(wm_concat(?1))"));
    }
}
3.4 使用

基于QueryDsl封装表达式接口。例如或运算,基于自定义方言我们在mysql上注册了bitand()而oracle则是在bitand()函数的基础上计算得到结果。封装表达式模板的接口如下:

@Autowired
private JpaDatabase database;
public NumberTemplate<Integer> bitor(Object arg1, Object arg2) {
    if(database.isMysql()){
        return Expressions.numberTemplate(Integer.class, "function('bitor', {0}, {1})", arg1, arg2);
    }
    if(database.isOracle()){
        return Expressions.numberTemplate(Integer.class, "({0}+{1})-function('bitand', {0}, {1})", arg1, arg2);
    }
    return null;
}

Expressions.numberTemplate是QueryDsl提供的方法,用于封装对应的表达式以调用我们在自定义方言的时候注册的一些函数。而对于分组,我们可以这样实现:

/**
 * concat函数
 * @param arg1
 * @return
 */
public StringTemplate concat(Object arg1){
    return Expressions.stringTemplate("function('concat', {0})", arg1);
}

四、效果

基于自定义数据库方言,我们就可以直接在QueryDsl的代码中调用封装的表达式方法,能在一定程度上简化代码,方便维护。
优化前:
浅谈JPA数据库方言适配优化后:浅谈JPA数据库方言适配最后解析成SQL语句的效果:浅谈JPA数据库方言适配浅谈JPA数据库方言适配