MyBatis 支持多个数据库

Posted

技术标签:

【中文标题】MyBatis 支持多个数据库【英文标题】:MyBatis support for multiple databases 【发布时间】:2019-05-02 10:30:41 【问题描述】:

我有不同的客户使用不同的数据库供应商(postgres、oracle、mysql 等)

我想编写一次我的代码并能够针对不同的数据库运行。

实现这一目标的“mybatis”方式是什么?

到目前为止我发现的问题例如:

postgres 在创建 sql 语句中有一个“如果不存在”的概念。 oracle 不支持。 oracle 在 sql 语法中没有“限制”和“偏移”支持,而其他 db 则支持。 DDL 语句中的文本(postgres)与 clob(oracle 和其他)jdbc 类型。

我不想重复我的查询(这是我迄今为止所做的)。可能有更优雅的方式来做到这一点。

我正在使用mybatis java注解。

【问题讨论】:

【参考方案1】:

您是说您正在使用注释,但我建议为此使用 XML。我通常觉得它更清楚,特别是因为无论如何您都需要完全更改查询的某些部分。

这是一个 DDL 示例,您可以为同一方法获取两个单独的 XML 元素,但数据库 ID 不同。这些表非常相似,但是由于您检查表是否已经存在以及类型大不相同的方式,您无法避免为此使用不同的 SQL 代码:

<update id="createTables" databaseId="postgresql">
    DO $$
    BEGIN
        CREATE TABLE IF NOT EXISTS item (
            id SERIAL PRIMARY KEY,
            content TEXT,
            creation_datetime TIMESTAMPTZ DEFAULT NOW(),
            modification_datetime TIMESTAMPTZ
        );
    END$$
</update>
<update id="createTables" databaseId="sqlserver">
    IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'item')
    BEGIN
        CREATE TABLE item (
            id INT IDENTITY PRIMARY KEY,
            content NVARCHAR(MAX),
            creation_datetime DATETIMEOFFSET DEFAULT SYSDATETIMEOFFSET(),
            modification_datetime DATETIMEOFFSET
        );
    END
</update>

您也可以在 SQL 元素中进行查询,但根据数据库 ID 使用 &lt;if&gt;(或 &lt;choose&gt;)更改不同的部分。如果更改很小,那效果很好:

<delete id="deleteItem">
    DELETE FROM my_item
    WHERE gid=CAST(#gid AS <if test="_databaseId == 'postgresql'">UUID</if><if test="_databaseId == 'sqlserver'">UNIQUEIDENTIFIER</if>)
</delete>

<select id="getLatestSomething" resultType="test.Something">
    SELECT <if test="_databaseId == 'sqlserver'">TOP 1</if> *
    FROM something
    ORDER BY creation_datetime DESC
    <if test="_databaseId == 'postgresql'">
    LIMIT 1
    </if>
</select>

在为不同的数据库 ID 使用单独的查询元素或在同一查询元素中仅使用条件片段之间进行选择是一个可读性问题。根据查询的复杂性,它可能非常主观。

例如,我发现以下使用 PostgreSQL 和 SQL Server 的“upsert”很难阅读。在单独的元素中会更好:

<insert id="insertStuff" parameterType="somestuff.Stuff">
    <if test="_databaseId == 'postgresql'">
        INSERT INTO my_stuff (...)
    </if>
    <if test="_databaseId == 'sqlserver'">
        MERGE INTO my_stuff WITH (HOLDLOCK) AS t USING (
    </if>
            VALUES (#...,
                    <if test="_databaseId == 'postgresql'">
                        CAST(#jsonData AS JSONB)
                    </if>
                    <if test="_databaseId == 'sqlserver'">
                        #jsonData
                    </if>
            )
    <if test="_databaseId == 'postgresql'">
            ON CONFLICT DO NOTHING
    </if>
    <if test="_databaseId == 'sqlserver'">
        )
              AS s (...)
              ON t....=s....
                 AND t....=s....
        WHEN NOT MATCHED BY TARGET THEN
            INSERT (...)
            VALUES (s...., s....);
    </if>
</insert>

MyBatis Dynamic SQL documentation 中有更多关于这一切的信息。


假设您的 XML 映射器文件位于 mypackage/MyMapper.xml 中,您可以在与该目录匹配的包中使用 MyMapper Java 接口。那里没有特定于数据库 ID 的内容。

package mypackage;

public interface MyMapper 
    void createTables();
    void deleteItem(@Param("gid") UUID gid);
    Something getLatestSomething();

当 MyBatis 与 Spring 一起使用时,您可以像这样设置供应商配置:

@Bean
public VendorDatabaseIdProvider vendorDatabaseIdProvider() 
    Properties vendorProperties = new Properties();
    vendorProperties.setProperty("PostgreSQL", "postgresql");
    vendorProperties.setProperty("SQL Server", "sqlserver");
    // Add others as required, this will look for the substring in the product name coming
    // from the database metadata.

    // ...
    VendorDatabaseIdProvider dbIdProvider = new VendorDatabaseIdProvider();
    dbIdProvider.setProperties(vendorProperties);
    return dbIdProvider;


@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext appContext,
        VendorDatabaseIdProvider vendorDatabaseIdProvider) throws Exception 
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setDatabaseIdProvider(vendorDatabaseIdProvider);

    SqlSessionFactory factory = bean.getObject();
    return factory;

如果您不使用 Spring,您应该可以使用 XML configuration 配置 DatabaseIdProvider

您并不严格需要数据库 ID 提供程序。它所做的只是根据从DataSource 获得的产品名称在配置中设置数据库 ID。类似的东西:

String databaseId = databaseIdProvider.getDatabaseId(dataSource);
configuration.setDatabaseId(databaseId);

(如果您查看org.apache.ibatis.mapping.VendorDatabaseIdProvider 的代码,您会看到databaseIdProvider.getDatabaseId(...) 只是在从DatabaseMetaData.getDatabaseProductName() 返回的内容中寻找匹配的子字符串。如果需要,您同样可以通过其他设置手动执行此操作。 )

注意,当databaseId=""直接用作XML元素的属性时,没有下划线,但当它用作测试条件时,则称为_databaseId

【讨论】:

感谢@Bruno。我基本上将查询定义为输入到 Mapper 接口上的 Select、Delete 等注释中的 Constands。所以 - 你是说有不同的 sql 语句是必须的吗?您能否分享您将使用哪个 Java api 来加载每个 DB 提供程序的“createTables”?我不熟悉这种方法 @YaOg 我不确定有没有办法自定义语句,仅仅是因为 MyBatis 允许您编写将发送到 RDBMS 的 SQL,这与 ORM 或其他中间层不同。控制 SQL 是 MyBatis 的一个特性,但缺点是您必须编写该 SQL 才能被您使用的特定 RDBMS 理解。通常您可以在同一个查询中进行简单的测试,但有时供应商之间的语法差异太大。

以上是关于MyBatis 支持多个数据库的主要内容,如果未能解决你的问题,请参考以下文章

mybatis查询oracle数据库数据异常

mybatis怎样批量插入数据到oracle,就算id自动增长问题

mybatis怎样批量插入数据到oracle,就算id自动增长问题

最近用MyBatis做开发的时候发现,MyBatis有个小小的缺点,不支持批量update?

最近用MyBatis做开发的时候发现,MyBatis有个小小的缺点,不支持批量update?

如何创建支持多个数据库的应用程序