MyBatis 核心配置讲解(下)

news2024/11/26 23:55:31

大家好,我是王有志,一个分享硬核 Java 技术的互金摸鱼侠。

我们书接上回,继续聊 MyBatis 的核心配置,我们今天分享剩下的 5 项核心配置。

不过正式开始前,我会先纠正上一篇文章 MyBatis 核心配置讲解(上)中出现的一个“错误”,并和大家说声抱歉。

勘误

首先和大家说声抱歉,在上一篇文章 MyBatis 核心配置讲解(上)中出现了一个“错误”,在演示自定义 ObjectFactory 时,我只是将数据库中的 gender 字段设置为 null,期望的查询结果中,应该只有 gender 被默认设置为“wyz”,可实际上在展示结果的图中,idNumer (数据库中为 id_number)也被默认设置为了“wyz”,同时查询结果中 idType(数据库中为 id_type)也没有展示出来。

这是因为,MyBatis 中并不会主动开启驼峰命名自动映射,即将数据库中的 id_number 字段映射为 Java 对象中的 idNumber。

要开启这个功能,需要我们在 MyBatis 的核心配置文件中,主动开启设置 mapUnderscoreToCamelCase,配置内容如下:

<configuration>
	<settings>
		<setting name="mapUnderscoreToCamelCase" value="true"/>
	</settings>
</configuration>

当我们将 MyBatis 与 Spring Boot 进行集成后,这个设置为:

mybatis:
  configuration:
    map-underscore-to-camel-case: true

对于上面的错误,再次向大家诚挚的说声抱歉!

objectWrapperFactory 元素

objectWrapperFactory 元素用于配置对象包装工厂(ObjectWrapperFactory),而 ObjectWrapperFactory 负责创建对象包装器(ObjectWrapper),ObjectWrapper 负责将 SQL 查询的结果集与 Java 对象的字段进行映射,它在 DTD 中的定义如下:

<!ELEMENT objectWrapperFactory EMPTY>
<!ATTLIST objectWrapperFactory
type CDATA #REQUIRED
>

objectWrapperFactory 元素只有一个子元素 objectWrapperFactory,用于配置 ObjectWrapperFactory 的实现,例如:

<configuration>
	<objectWrapperFactory type="com.wyz.customize.factory.wrapper.CustomizeObjectWrapperFactory"/>
</configuration>

可以看到,这里我配置的是 CustomizeObjectWrapperFactory,即自定义的 ObjectWrapper 工厂,那么下面我们就实现这个 CustomizeObjectWrapperFactory。

首先我们要先实现对象包装器,在 MyBatis 中只需要继承 ObjectWrapper 接口即可,这里我们偷个懒,直接继承 ObjectWrapper 的实现类 BeanWrapper,代码如下:

package com.wyz.customize.wrapper.object;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.reflection.wrapper.BeanWrapper;


public class CustomizeObjectWrapper extends BeanWrapper {

	public CustomizeObjectWrapper(MetaObject metaObject, Object object) {
		super(metaObject, object);
	}

	@Override
	public void set(PropertyTokenizer prop, Object value) {
		super.set(prop, value);
	}

	@Override
	public String findProperty(String name, boolean useCamelCaseMapping) {
		return super.findProperty(name, useCamelCaseMapping);
	}

	@Override
	public Class<?> getSetterType(String name) {
		return super.getSetterType(name);
	}

	@Override
	public boolean hasSetter(String name) {
		return super.hasSetter(name);
	}

	@Override
	public boolean hasGetter(String name) {
		return super.hasGetter(name);
	}
}

这里我只保留了几个关键方法:

  • BeanWrapper#set,用于将查询到的数据库结果集中字段的值赋值到 Java 对象的实例的指定属性上;
  • BeanWrapper#findProperty,用于获取数据库表中字段在 Java 对象中的映射,例如:数据库表中的 user_id 映射到 Java 对象中的 userId;
  • BeanWrapper#getSetterType,用于获取 Java 对象中字段的类型;
  • BeanWrapper#hasSetterBeanWrapper#hasGetter,用于判断 Java 对象中的属性是否有 getter 方法和 setter 方法。

接着我们来实现对象包装器工厂,同样的 MyBatis 中也提供了 ObjectWrapperFactory 接口,我这里同样选择继承 ObjectWrapperFactory 接口的实现类 DefaultObjectWrapperFactory,代码如下:

package com.wyz.customize.factory.wrapper;

import com.wyz.customize.wrapper.object.CustomizeObjectWrapper;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapper;

public class CustomizeObjectWrapperFactory extends DefaultObjectWrapperFactory {

    public CustomizeObjectWrapperFactory() {
        super();
    }

    @Override
    public boolean hasWrapperFor(Object object) {
        return true;
    }

    @Override
    public ObjectWrapper getWrapperFor(MetaObject metaObject, Object object) {
        return new CustomizeObjectWrapper(metaObject, object);
    }
}

ObjectWrapperFactory 接口的核心方法是ObjectWrapperFactory#getWrapperFor,用于从对象包装器工厂中返回对象包装器,因此该方法中我没有直接调用父类的视线,而是返回我们自定义对象包装器 CustomizeObjectWrapper 的实例。

在上一篇MyBatis 核心配置讲解(上)中我们已经知道,ObjectFactory 是用于创建 SQL 语句的结果集映射到的 Java 对象的实例,那么 ObjectWrapper 就是用于为已经创建的 Java 对象实例中的每个字段进行赋值的

同样的,MyBatis 中提供的 ObjectWrapper 也已经满足了绝大部分场景,因此通常来说,我们也不太需要自定义 ObjectWrapper。

plugins 元素

plugins 元素用于配置 MyBatis 的插件,MyBatis 的插件会在 SQL 语句执行过程中的某一时间点进行拦截调用,它在 DTD 中的定义如下:

<!ELEMENT plugins (plugin+)>

<!ELEMENT plugin (property*)>
<!ATTLIST plugin
interceptor CDATA #REQUIRED
>

plugins 元素只有一个子元素 plugin,用于配置插件的具体实现类,假设我们有一个自定义插件,用来转换查询响应结果中的性别字段,将数据库存储的性别码值转换为“男/女”,该插件的类名为 ConvertGenderPlugin,那么我们可以这样配置:

<configuration>
  <plugins>
    <plugin interceptor="com.wyz.customize.plugin.ConvertGenderPlugin">
      <property name="M" value=""/>
      <property name="F" value=""/>
      <property name="0" value=""/>
      <property name="1" value=""/>
    </plugin>
  </plugins>
</configuration>

MyBatis 为插件提供了非常丰富的拦截场景(接口)和拦截时机(方法):

场景(接口)时机(方法)
Executorupdate, query, flushStatements, commit, rollback, getTransaction, close, isClosed
ParameterHandlergetParameterObject, setParameters
ResultSetHandlerhandleResultSets, handleOutputParameters
StatementHandlerprepare, parameterize, batch, update, query

自定义插件

接下来我们实现上述的转换性别字段的插件,先说一下整体思路,我这里选择在拦截Executor#query方法,在获取到查询结果后,通过反射获取结果中的性别字段(gender),并将码值转换为“男/女”的文字描述。当然了,这个功能使用其它的方式会更加合理,不过我这里只是为了演示如何自定义插件,就不要求合理性了。

自定义插件需要实现 Interceptor 接口,并通过@Intercepts注解声明被拦截的类型和被拦截的方法,我这里实现的 ConvertGenderPlugin 源码如下:

package com.wyz.customize.plugin;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.util.*;

@Slf4j
@Intercepts({
  @Signature(
    type = Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ConvertGenderPlugin implements Interceptor {

  private Map<String, String> genderMap;

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    log.info("[自定义插件]性别转换,M/F转换为男/女");
    Object result = invocation.proceed();
    String className = result.getClass().getName();

    if (result instanceof Collection<?> results) {
      for (Object tempResult : results) {
        setGender(tempResult);
      }
    } else if (className.startsWith("java")) {
      return result;
    } else {
      setGender(result);
    }
    return result;
  }

  private void setGender(Object result) throws IllegalAccessException {
    Field[] fields = result.getClass().getDeclaredFields();
    for (Field field : fields) {
      if ("gender".equals(field.getName())) {
        Class<?> fieldType = field.getType();
        if (String.class.equals(fieldType)) {
          field.setAccessible(true);
          String value = (String) field.get(result);
          field.set(result, this.genderMap.get(value));
        }
      }
    }
  }

  @Override
  public Object plugin(Object target) {
    return Interceptor.super.plugin(target);
  }

  @Override
  public void setProperties(Properties properties) {
    if (this.genderMap == null) {
      this.genderMap = new HashMap<>();
    }
    properties.forEach((key, value) -> genderMap.put((String) key, (String) value));
  }
}

可以看到,在类的声明上,我使用了@Intercepts注解,并通过@Signature注解声明了插件的拦截场景和拦截时机,具体到 MyBatis 的源码中是 Executor 的如下方法:

public interface Executor {
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
}

结合上述源码,我们能够很清晰的了解到@Signature注解每个参数的意义:

  • type,被拦截的接口;
  • method,被拦截接口的方法名称;
  • args,被拦截方法的参数(用于区分重载方法)。

最后,我们执行单元测试,可以看到查询出来的结果中,gender 字段已经被转换为了“男/女”。

image.png

Tips:如果你通过单元测试执行出来的结果没有成功的转换 gender 字段,你可能需要把之前我们在 MyBatis 核心配置中配置的 typeHandlers 元素注释掉。

environments 元素

environments 元素用于配置 MyBatis 应用的运行环境,它在 DTD 中的定义如下:

<!ELEMENT environments (environment+)>

environments 元素只有一个子元素 environment,在 MyBatis 应用中 environment 元素需要至少进行一次配置,但 MyBaits 应用也允许配置多个 environment。另外,environments 元素还有一个属性 defalut,用于指定 MyBatis 应用默认的运行环境,例如:

<environments default="MySQL_environment">
  <environment id="MySQL_environment">
    <!-- 省略配置-->
  </environment>
</environments>

上述的配置中,我们指定了 MyBatis 应用默认的运行环境为“MySQL_environment”。

environment 元素

environment 元素用于配置 MyBaits 应用具体的运行环境,它在 DTD 中的定义如下:

<!ELEMENT environment (transactionManager,dataSource)>
<!ATTLIST environment
id CDATA #REQUIRED
>

environment 元素中有一个属性 id,用于指定该运行环境的 id,以及两个子元素:transactionManager 元素和 dataSource 元素。

transactionManager 元素

transactionManager 元素用于配置数据库事务,它在 DTD 中的定义如下:

<!ELEMENT transactionManager (property*)>
<!ATTLIST transactionManager
type CDATA #REQUIRED
>

MyBatis 的原生应用中提供了两种数据库事务类型:

  • JDBC,直接使用 JDBC 的 commit 和 rollback,通过数据库来管理事务,由 MyBatis 中的 JdbcTransactionFactory 和 JdbcTransaction 配合实现;
  • MANAGED,commit 和 rollback 时什么也不做,而是交由容器来管理事务,,由 MyBatis 中的 ManagedTransactionFactory 和 ManagedTransaction 配合实现。

通常情况下,在原生 MyBatis 应用中,我们会选择使用 JDBC,而在 MyBatis 与 Spring 或 Spring Boot 集成后,我们不需要配置事务管理,Spring 或 Spring Boot 会托管事务管理的功能。

transactionManager 元素在 MyBatis 核心配置文件中的配置如下:

<configuration>
  <environments default="MySQL_environment">
    <environment id="MySQL_environment">
      <transactionManager type="JDBC"/>
      <!-- 省略其它配置 -->
    </environment>
  </environments>
</configuration>
dataSource 元素

dataSource 元素用于配置数据源信息,它在 DTD 中的定义如下:

<!ELEMENT dataSource (property*)>
<!ATTLIST dataSource
type CDATA #REQUIRED
>

MyBatis 的原生应用中提供了 3 中数据源的配置:

  • UNPOOLED,不使用数据库连接池的数据源,每次请求都会开启新的数据库连接,由 MyBatis 中的 UnpooledDataSourceFactory 和 UnpooledDataSource 配合实现;
  • POOLED,使用数据库连接池的数据源,利用池化技术控制数据库连接,避免了频繁创建数据库连接带来的性能消耗,由 MyBatis 中的 PooledDataSourceFactory 和PooledDataSource 配合实现;
  • JNDI,JNDI 数据源,由容器在外部配置数据源,通过 JNDI 上下文引用,由 MyBatis 中的 JndiDataSourceFactory 实现。

通常情况下,我们首选 POOLED 类型的数据源配置,利用数据库连接池来优化性能。dataSource 元素在 MyBatis 核心配置文件中的配置如下:

<configuration>
  <properties resource="mysql-config.properties"/>

  <environments default="MySQL_environment">
    <environment id="MySQL_environment">
      <dataSource type="POOLED">
        <property name="driver" value="${mysql.driver}"/>
        <property name="url" value="${mysql.url}"/>
        <property name="username" value="${mysql.username}"/>
        <property name="password" value="${mysql.password}"/>
      </dataSource>
    </environment>
  </environments>
</configuration>

需要注意的是,数据库连接池并不是数据库提供的能力而是程序在数据源的基础上提供的能力,因此你在阅读 MyBatis 的源码时会发现 POOLED 类型的数据源 PooledDataSource 内部使用了 UnpooledDataSource,PooledDataSource 的部分源码如下:

public class PooledDataSource implements DataSource {
  private final UnpooledDataSource dataSource;

  public PooledDataSource() {
    dataSource = new UnpooledDataSource();
  }

  public PooledDataSource(String driver, String url, String username, String password) {
    dataSource = new UnpooledDataSource(driver, url, username, password);
    expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
  }
}

因此,在 dataSource 元素的配置中,POOLED 和 UNPOOLED 类型的数据源(即 PooledDataSource 和 UnpooledDataSource)会共享部分配置,完整的配置列表如下:

配置取值示例说明数据源类型
drivercom.mysql.cj.jdbc.Driver数据库驱动POOLED 和 UNPOOLED
urljdbc:mysql://localhost:3306/mybatis数据库地址POOLED 和 UNPOOLED
usernameroot数据库用户名POOLED 和 UNPOOLED
password123456数据库密码POOLED 和 UNPOOLED
defaultTransactionIsolationLevel0默认的事务隔离级别,java.sql.Connection 接口中定义的事务隔离级别POOLED 和 UNPOOLED
defaultNetworkTimeout10000数据库连接的超时时间(单位毫秒)POOLED 和 UNPOOLED
loginTimeout10000数据库登录的超时时间(单位毫秒)UNPOOLED
autoCommittrue是否自动提交,默认为 falseUNPOOLED
poolMaximumActiveConnections10数据库连接池最大活跃连接数(默认 10 个)POOLED
poolTimeToWait10000获取数据库连接的最大时间,超时会重连(默认 20000 毫秒)POOLED
poolMaximumIdleConnections10数据库连接池最大空闲连接数POOLED
poolPingEnabledtrue是否开启数据库连接检测(默认 false)
poolPingQueryselect 1 from用于检测数据库连接是否正常的 SQL 语句POOLED
poolPingConnectionsNotUsedFor10000检查连接是否可用的时间间隔(单位毫秒)POOLED
poolMaximumLocalBadConnectionTolerance3如果从连接池中获取到不可用的连接,允许进行重试的阈值(poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance),超过阈值依旧无法获取到可用连接则抛出异常,poolMaximumLocalBadConnectionTolerance 默认值 3POOLED
poolMaximumCheckoutTime20000从数据库连接池获取连接的最长等待时间,默认 20000,单位毫秒POOLED

dataSource 元素的配置项是通过反射读取 DataSource 的实现类中以 set 开头的方法并截取该方法名中 set 之后的内容获取的。注意,PooledDataSource 中有些 set 方法无法通过 MyBatis 的核心配置文件进行配置,例如:logWriter。

因为 UnpooledDataSource 的相关配置较少,我们就以 UnpooledDataSource 的源码为例:

public class UnpooledDataSourceFactory implements DataSourceFactory {

  private ClassLoader driverClassLoader;
  private Properties driverProperties;

  private String driver;
  private String url;
  private String username;
  private String password;

  private Boolean autoCommit;
  private Integer defaultTransactionIsolationLevel;
  private Integer defaultNetworkTimeout;

  @Override
  public void setLoginTimeout(int loginTimeout){
    DriverManager.setLoginTimeout(loginTimeout);
  }

  @Override
  public void setLogWriter(PrintWriter logWriter) {
    DriverManager.setLogWriter(logWriter);
  }

  public void setDriverClassLoader(ClassLoader driverClassLoader) {
    this.driverClassLoader = driverClassLoader;
  }

  public void setDriverProperties(Properties driverProperties) {
    this.driverProperties = driverProperties;
  }

  public synchronized void setDriver(String driver) {
    this.driver = driver;
  }

  public void setUrl(String url) {
    this.url = url;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public void setAutoCommit(Boolean autoCommit) {
    this.autoCommit = autoCommit;
  }

  public void setDefaultTransactionIsolationLevel(Integer defaultTransactionIsolationLevel) {
    this.defaultTransactionIsolationLevel = defaultTransactionIsolationLevel;
  }

  public void setDefaultNetworkTimeout(Integer defaultNetworkTimeout) {
    this.defaultNetworkTimeout = defaultNetworkTimeout;
  }
}

其中最明显的是 UnpooledDataSourceFactory 中并没有 loginTimeout 和 logWriter 这两个字段,但是我们却可以在 dataSource 元素中配置。关于 MyBatis 中获取 dataSource 元素配置的具体实现,我会在后续的源码分析篇中和大家分享的。

自定义事务处理器

除了 MyBatis 提供的两种事务处理器外,我们还可以实现自定义的事务处理器。

实现自定义事务处理器,我们需要实现 Transaction 接口,这里我们只是为了展示如何实现自定义事务处理器,所以就偷个懒,选择继承 Transaction 的实现类 JdbcTransaction,并仅仅在获取连接和关闭连接时输出日志,其余方法直接调用 JdbcTransaction,代码如下:

package com.wyz.customize.transaction;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.jdbc.JdbcTransaction;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

@Slf4j
public class CustomizeTransaction extends JdbcTransaction {
  public CustomizeTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
    super(ds, desiredLevel, desiredAutoCommit);
  }

  public CustomizeTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit, boolean skipSetAutoCommitOnClose) {
    super(ds, desiredLevel, desiredAutoCommit, skipSetAutoCommitOnClose);
  }

  public CustomizeTransaction(Connection connection) {
    super(connection);
  }

  @Override
  public Connection getConnection() throws SQLException {
    log.info("[自定义事务管理器]获取连接");
    return super.getConnection();
  }

  @Override
  public void close() throws SQLException {
    log.info("[自定义事务管理器]关闭连接");
    super.close();
  }
}

接着我们为 CustomizeTransaction 定义工厂类,这里需要实现 TransactionFactory 接口,我们的实现依旧很简单,打印日志,并且创建事务管理器 CustomizeTransaction,代码如下:

package com.wyz.customize.transaction;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.checkerframework.checker.units.qual.C;

import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Properties;

@Slf4j
public class CustomizeTransactionFactory implements TransactionFactory {

  public CustomizeTransactionFactory() {
  }

  @Override
  public Transaction newTransaction(Connection conn) {
    log.info("[自定义事务处理器工厂]创建事务处理器");
    return new CustomizeTransaction(conn);
  }

  @Override
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    log.info("[自定义事务处理器工厂]创建事务处理器");
    return new CustomizeTransaction(ds, level, autoCommit);
  }
}

最后,我们需要在 MyBatis 的核心配置文件中配置我们的事务处理器。实际上,MyBatis 核心配置文件中配置的是事务处理器工厂,并且我们需要借助 typeAliases 元素,配置如下:

<configuration>
  <typeAliases>
    <typeAlias alias="CUSTOM" type="com.wyz.customize.transaction.CustomizeTransactionFactory"/>
  </typeAliases>

  <environments default="MySQL_environment">
    <environment id="MySQL_environment">
      <transactionManager type="CUSTOM"/>
      <dataSource type="POOLED">
        <!-- 省略dataSource配置 --> 
      </dataSource>
    </environment>
  </environments>
</configuration>

通过测试,我们可以看到日志中正常输出了在自定义事务处理器工厂和自定义事务处理器中的日志:

image.png

自定义数据源

自定义数据源的方式与自定义事务处理器的整体流程一致,只不过是需要实现的接口不同。自定义数据源需要实现 Java 提供的 DataSource 接口,而自定义数据源工厂实现的是 MyBatis 提供的接口 DataSourceFactory。

首先,我们实现一个自定义数据源 CustomizeDataSource,同样偷个懒,我们直接继承 PooledDataSource,只修改下获取连接的方法,添加一行日志输出,代码如下:

package com.wyz.customize.source;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.datasource.pooled.PooledDataSource;

import java.sql.Connection;
import java.sql.SQLException;

@Slf4j
public class CustomizeDataSource extends PooledDataSource {

    @Override
    public Connection getConnection() throws SQLException {
        log.info("[自定义数据源]获取链接");
        return super.getConnection();
    }
}

接着我们实现自定义数据源工厂 CustomizeDataSourceFactory,同样选择继承 MyBatis 已经实现的 PooledDataSourceFactory,依旧是添加一行日志输出,代码如下:

package com.wyz.customize.source;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.datasource.pooled.PooledDataSource;
import org.apache.ibatis.datasource.pooled.PooledDataSourceFactory;

import javax.sql.DataSource;
import java.util.Properties;

@Slf4j
public class CustomizeDataSourceFactory extends PooledDataSourceFactory {

    public CustomizeDataSourceFactory() {
        log.info("[自定义数据源工厂]创建数据源");
        this.dataSource = new CustomizeDataSource();
    }
}

最后的配置方式与自定义事务处理器相同,也是需要通过 typeAliases 元素声明自定义数据源工厂的别名,配置如下:

<configuration>
  <typeAliases>
    <typeAlias alias="CUSTOM_POOLED" type="com.wyz.customize.source.CustomizeDataSourceFactory"/>
  </typeAliases>
  <environments default="MySQL_environment">
    <environment id="MySQL_environment">
      <transactionManager type="CUSTOM"/>
      <dataSource type="CUSTOM_POOLED">
        <! -- 省略数据源配置 -->
      </dataSource>
    </environment>
  </environments>
</configuration>

通过测试,我们可以看到日志中正常输出了在自定义数据源工厂和自定义数据源中的日志:

image.png

至于为什么自定义事务处理器(工厂)和自定义数据源(工厂)需要借助 typeAliases 元素,这与 MyBatis 的实现方式有关,后面我会在介绍 MyBatis 配置解析的源码中再和大家详细的分析。

配置多数据源

我们注意到 environments 元素是复数形式,并且 MyBatis 对 environments 元素的子元素 environment 的要求是至少进行一次配置,那也就是说我们可以在 MyBatis 的中配置多个运行环境。这里我们为测试程序配置两个不同数据库的运行环境,分别是连接 MySQL 的运行环境和连接 PostgreSQL 的运行环境。

正式配置多数据源之前,我们先要引入 PostgreSQL 的依赖,我们在 POM 文件中做如下配置:

<dependency>
  <groupId>org.postgresql</groupId>
  <artifactId>postgresql</artifactId>
  <version>42.7.3</version>
</dependency>

接着我们在 MyBatis 的核心配置文件中配置多数据源,如下:

<configuration>
  <environments default="MySQL_environment">
    <environment id="MySQL_environment">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
    <environment id="PostgreSQL_environment">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="org.postgresql.Driver"/>
        <property name="url" value="jdbc:postgresql://localhost:5432/mybatis"/>
        <property name="username" value="postgres"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
  </environments>
</configuration>

这里我们已经配置了 MySQL 的运行环境和 PostgreSQL 运行环境,接着我们来写映射器中的 SQL 语句,代码如下:

<mapper namespace="com.wyz.mapper.UserMapper">
  <!--MySQL 运行环境的 SQL 语句 -->
  <select id="selectAll" resultType="com.wyz.entity.UserDO">
    select user_id, name, age, gender, id_type, id_number from user
  </select>

  <!-- PostgreSQL 运行环境的 SQL 语句 --> 
  <select id="selectFirstUser" resultType="com.wyz.entity.UserDO">
    select user_id, name, age, gender, id_type, id_number from mybatis.user where user_id = 1
  </select>
</mapper>

这里我在 PostgreSQL 中创建了于 MySQL 中同样的的 user 表,需要注意,在 PostgreSQL 的 SQL 语句中,我使用的是“mybatis.user”,这是因为 PostgreSQL 的数据库结构与 MySQL 的并不完全相同,PostgreSQL 是 3 层结构,而 MySQL 是两层结构:

最后我们来写单元测试:

package com.wyz.mapper;

import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.io.Reader;
import java.sql.SQLOutput;

@Slf4j
public class MultipleDataSourcesTest {

  private static SqlSessionFactory mysqlFactory;

  private static SqlSessionFactory postgresqlFactory;

  @BeforeClass
  public static void init() throws IOException {
    Reader mysqlReader = Resources.getResourceAsReader("mybatis-config.xml");
    mysqlFactory = new SqlSessionFactoryBuilder().build(mysqlReader);

    Reader postgresqlReader = Resources.getResourceAsReader("mybatis-config.xml");
    postgresqlFactory = new SqlSessionFactoryBuilder().build(postgresqlReader, "PostgreSQL_environment");
  }

  @Test
  public void testSelect() {
    SqlSession mysqlSession = mysqlFactory.openSession();
    UserMapper mysqlUserMapper = mysqlSession.getMapper(UserMapper.class);
    mysqlUserMapper.selectAll().forEach(userDO -> log.info("MySQL 查询结果: {}", JSON.toJSONString(userDO)));

    SqlSession postgresqlSession = postgresqlFactory.openSession();
    UserMapper postgresqlUserMapper = mysqlSession.getMapper(UserMapper.class);
    log.info("PostgreSQL 查询结果:{}", JSON.toJSONString(postgresqlUserMapper.selectFirstUser()));

    mysqlSession.close();
    postgresqlSession.close();
  }
}

可以看到,我们分别为 MySQL 的运行环境和 PostgreSQL 的运行环境创建了 mysqlFactory 和 postgresqlFactory,这是因为,虽然 MyBatis 支持多数据源,但是每一个 SqlSessionFactory 的实例只能使用一个运行环境

databaseIdProvider 元素

databaseIdProvider 用于指定不同 SQL 语句运行的数据库环境,它在 DTD 中的定义如下:

<!ELEMENT databaseIdProvider (property*)>
<!ATTLIST databaseIdProvider
type CDATA #REQUIRED
>

databaseIdProvider 提供了一个属性 type,通常我们使用 MyBatis 的内置配置“DB_VENDOR”即可,该配置是 VendorDatabaseIdProvider 在 MyBatis 中的别名,提供了获取数据源对应的数据库名称的方法

同样的,我们可以对 dataBaseIdProvider 进行自定义,只需要实现 DatabaseIdProvider 接口即可,配置流程自定义事务处理器和自定义数据源相同,都需要借助 typeAliases 元素来完成,这里我们就不再展示了。

databaseIdProvider 的使用非常简单,只需要配置数据库名称与别名即可,这里以我们配置多数据源的 MyBatis 应用为例,为 MySQL 和 PostgreSQL 配置别名:

<configuration>
  <databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="PostgreSQL" value="postgresql"/>
  </databaseIdProvider> 
</configuration>

接着我们需要在映射器中,指定 SQL 语句对应的数据库,例如:

<mapper namespace="com.wyz.mapper.UserMapper">
  <select id="selectAll" resultType="com.wyz.entity.UserDO" databaseId="mysql">
    select user_id, name, age, gender, id_type, id_number from user
  </select>

  <select id="selectFirstUser" resultType="com.wyz.entity.UserDO" databaseId="postgresql">
    select user_id, name, age, gender, id_type, id_number from mybatis.user where user_id = 1
  </select>
</mapper>

现在我们使用在配置多数据源时的单元测试进行测试,可以看到测试正常通过。可能有些小伙伴会产生疑惑,这跟不使用 databaseIdProvider 有什么区别?

我们来修改下单元测试,让通过 MySQL 数据源获取到的连接执行UserMapper#selectFirstUser方法,而通过 PostgreSQL 数据源获取到的连接执行UserMapper#selectAll方法,代码如下:

@Test
public void testSelect() {
  SqlSession mysqlSession = mysqlFactory.openSession();
  UserMapper mysqlUserMapper = mysqlSession.getMapper(UserMapper.class);
  log.info("MySQL 查询结果:{}", JSON.toJSONString(mysqlUserMapper.selectFirstUser()));

  SqlSession postgresqlSession = postgresqlFactory.openSession();
  UserMapper postgresqlUserMapper = postgresqlSession.getMapper(UserMapper.class);
  postgresqlUserMapper.selectAll().forEach(userDO -> log.info("PostgreSQL 查询结果: {}", JSON.toJSONString(userDO)));

  mysqlSession.close();
  postgresqlSession.close();
}

通过测试,可以看到编译没问题,但是执行却报错了。

image.png

这是因为UserMapper#selectAll方法被绑定到了 MySQL 数据源上,而UserMapper#selectFirstUser方法被绑定到了 PostgreSQL 数据源上,不同数据源的 SqlSessionFactory 在读取映射器文件时,只会读取 dataBaseId 与自己匹配的,或者是没有绑定 dataBaseId 的 SQL 语句,这是在 MyBatis 读取配置构建 SqlSessionFactory 的 Configuration 时处理的,如下图不同数据源构建的 Configuration 所示:

WPS拼图0.png

mappers 元素

mappers 元素用于配置 MyBatis 应用的映射器(Mapper.xml),它在 DTD 中的定义如下:

<!ELEMENT mappers (mapper*,package*)>

mappers 元素没有任何属性,只有两个子元素:mapper 元素和 package 元素。

mapper 元素

mapper 元素提供了 3 个属性,它在 DTD 中的定义如下:

<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>

mapper 的 3 个属性代表了 3 种配置 MyBatis 映射器的方式。

resource 属性

首先是通过属性 resource 配置,这也是我们在 MyBatis 入门中使用的方式,这种方式我们直接使用映射器文件(Mapper.xml)的相对路径即可,例如:

<mappers>
  <mapper resource="mapper/UserMapper.xml"/>
</mappers>
url 属性

使用 url 时,需要使用映射器文件(Mapper.xml)的完全限定资源定位符,例如:

<mappers>
  <mapper url="file:///F://Project/MyBatis-Tradition/src/main/resources/mapper/UserMapper.xml"/>
</mappers>
class 属性

class 属性允许我们使用 MyBaits 映射器文件(Mapper.xml)对应的 Java 接口进行配置,例如:

<mappers>
  <mapper class="com.wyz.mapper.UserMapper"/>
</mappers>

但是在使用 class 属性配置映射器时,有两点需要注意:

  • 映射器文件(Mapper.xml)必须与对应的 Java 接口名称相同;
  • 映射器文件(Mapper.xml)必须与对应的 Java 接口位于同一个目录下。

如下,就是一个使用 class 属性配置映射器文件(Mapper.xml)的合法方式:

image.png

纵观 mapper 元素的属性,3 种配置方式都只能完成一个映射器的配置,如果项目中只有一两个映射器文件的话,我们还能应付的过来,但是如果有几十个上百个映射器文件的话,我们还要一个一个的配置吗?

package 元素

package 元素提供了一个属性,它在 DTD 中的定义如下:

<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>

package 元素允许我们通过配置映射器文件对应的 Java 接口所在的包名进行统一配置,例如:

<mapper>
  <package name="com.wyz.mapper"/>
</mapper>

上述的配置方式,会将 com.wyz.mapper 包下所有的映射器文件对应的 Java 接口加载的 MyBatis 应用程序中,但是与 mapper 元素的 class 属性一样,也需要遵循映射器文件的名称与 Java 接口的名称一致,且位于同一个文件目录下

使用 package 虽然可以批量加载映射器,但是依旧存在问题,通常 Java 接口与映射器文件不会位于同一个目录下,而 package 元素会破坏这种分开存放的方式,那么有没有办法解决呢?

目前 MyBatis 的原生应用中没有办法解决 Java 接口与映射器分开存放且批量加载的问题,但是一旦将 MyBatis 与 Spring 或 Spring Boot 集成后,我们就可以使用@Mapper注解和@MapperScan注解来解决这个问题,不过这是后话了。

至此,我们就把 MyBatis 的核心配置文件中的所有元素的定义和用法都讲解完了,下一篇我们一起来学习 MyBatis 映射器中的元素。


好了,今天的内容就到这里了,如果本文对你有帮助的话,希望多多点赞支持,如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核 Java 技术的金融摸鱼侠王有志,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1631923.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【链表——数据结构】

文章目录 1.单链表1.定义2.基本操作2.1.不带头结点2.2后插2.3前插2.4删除2.5按位查找2.6按值查找2.7求单链表长度2.8 建表 2.双链表1.初始化2.插入(后插)3.删除(后删)4.遍历 3.循环链表1.循环单链表2.循环双链表3.代码问题 4.静态链表1.简述基本操作的实现1.初始化3.删除某个结…

【AIGC调研系列】Sora级别的国产视频大模型-Vidu

Vidu能够达到Sora级别的标准。Vidu被多个来源认为是国内首个Sora级别的视频大模型[2][3][4]。它采用了团队原创的Diffusion与Transformer融合的架构U-ViT&#xff0c;能够生成长达16秒、分辨率高达1080P的高清视频内容[1][6]。此外&#xff0c;Vidu的一致性、运动幅度都达到了S…

vue2如何创建一个项目?

目录 1. 安装环境&#xff1a; 2. 安装Vue CLI 3. 创建新项目 4. 选择配置 5. 安装依赖并运行 6. 开始开发 7. 构建项目 8. 预览生产环境构建 首先创建一个vue2项目&#xff0c;你可以通过以下步骤进行&#xff1a; 1. 安装环境&#xff1a; 保证自己的电脑已经安装N…

Jmeter Beanshell 设置全局变量

//获取token import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONArray; import java.util.*; import org.apache.jmeter.util.JMeterUtils; //获取可上机机器 String response prev.getResponseDataAsString(); JSONObject responseObect JSONObjec…

rancher/elemental 构建不可变IOS(一)

一、什么是elemental Elemental 是 Rancher 的一个变种&#xff0c;专注于提供一个更轻量级的 Kubernetes 发行版。它旨在提供简化的部署和管理体验&#xff0c;同时保持 Kubernetes 的灵活性和强大功能。Elemental 通常针对较小的部署场景或资源受限的环境&#xff0c;例如测…

PY32F040单片机产品介绍,LQFP封装,带LCD 驱动器

PY32F040单片机搭载了 Arm Cortex-M0内核&#xff0c;最高主频可达72 MHz&#xff0c;专为高性价比、高可靠性的系统而设计&#xff0c;符合消费市场的基本设计需求。可广泛应用于电机控制、手持设备、PC 外设、以及复杂的数字控制应用等领域。 PY32F040片内集成 UART、I2C、S…

Pycharm配深度学习环境所遇到的部分问题

问题1&#xff1a;Anaconda prompt界面安装CUDA出现的问题: 不管是&#xff1a;conda install pytorch torchvision torchaudio cudatoolkit11.3 -c pytorch 还是:pip ****什么的 问题描述&#xff1a;EnvironmentNotWritableError: The current user does not have write p…

手动在Ubuntu22.04上部署LAMP环境

简介 LAMP环境是常用的Web开发环境之一&#xff0c;其中LAMP分别代表Linux、Apache、MySQL和PHP。本文介绍如何在Ubuntu操作系统的ECS实例内部署LAMP环境。 准备工作 该实例必须满足以下条件&#xff1a; 实例已分配公网IP地址或绑定弹性公网IP&#xff08;EIP&#xff09;。…

【Java】java实现文件上传和下载(上传到指定路径/数据库/minio)

目录 上传到指定路径 一、代码层级结构 二、文件上传接口 三、使用postman进行测试&#xff1b; MultipartFile接收前端传递的文件&#xff1a;127.0.0.1:8082/path/uploadFile part接收前端传递的文件&#xff1a;127.0.0.1:8082/path/uploadFileByRequest 接收前端传递…

【存储芯片】CS创世 SD NAND:可以机贴的存储芯片

什么是CS创世 SD NAND呢&#xff1f;很多的朋友一直想知道这个问题。今天精心准备了SD NAND 的一个介绍。其实很多工程师朋友对CS创世 SD NAND有很多称呼。比如&#xff1a;贴片式T卡、贴片式TF卡、贴片式SD卡、可焊接的T卡&#xff0c;可焊接的SD卡&#xff0c;可贴片的TF卡&a…

TikTok引流中海外云手机的实用功能分享

在当下&#xff0c;TikTok已成为全球范围内最受欢迎的社交媒体平台之一&#xff0c;拥有着庞大的用户群体和潜在的商业机会。为了在TikTok上实现更好的引流效果&#xff0c;利用海外云手机成为了一个明智的选择。接下来&#xff0c;我们将深入探讨海外云手机的功能以及它如何助…

LLM优化:开源星火13B显卡及内存占用优化

1. 背景 本qiang~这两天接了一个任务&#xff0c;部署几个开源的模型&#xff0c;并且将本地经过全量微调的模型与开源模型做一个效果对比。 部署的开源模型包括&#xff1a;星火13B&#xff0c;Baichuan2-13B, ChatGLM6B等 其他两个模型基于transformers架构封装&#xff0…

创建基于时间的 UUID

概述 在本文中&#xff0c;我们将会 对 UUIDs 和基于时间的 UUIDs&#xff08;time-based UUIDs&#xff09; 进行一些探讨。 当我们在对基于时间的 UUIDs 进行选择的时候&#xff0c;总会遇到一些好的方面和不好的方面&#xff0c;如何进行选择&#xff0c;也是我们将要简要…

代码+视频,R语言绘制生存分析模型的时间依赖(相关)性roc曲线和时间依赖(相关)性cindex曲线

ROC曲线分析是用于评估一个因素预测能力的手段&#xff0c;是可以用于连续型变量分组的方法。在生存分析中&#xff0c;疾病状态和因素取值均会随时间发生变化。而标准的ROC曲线分析将个体的疾病状态和因素取值视作固定值&#xff0c;未将时间因素考虑在分析之中。在这种情况下…

一加Ace3/12/Ace2pro手机ColorOS14刷KernelSU内核ROOT-解决无限重启变砖

一加Ace3/一加12/一加11等手机升级了安卓14底层&#xff0c;并且ColorOS版本也更新到了14版本界面和功能都比之前的系统表现更加优秀&#xff0c;但刷机方面&#xff0c;相对之前存在一些差异&#xff0c;特别是KernelSU内核级别root权限&#xff0c;不再支持一键刷入KernelSU通…

【Linux网络】SSH--远程控制与访问

目录 一、SSH远程管理 1.SSH的定义 2.远程传输的种类 3.OpensSSH 4.SSH客户端与服务端 二、配置OpenSSH服务器 1.sshd_config配置文件的常用选项设置 2.sshd 服务支持两种验证方式 1&#xff09;密码验证 2&#xff09;密钥对验证 三、使用 SSH 客户端程序 1.ssh 远…

从 Sora 制作的短片看AI生成视频的优势与局限性解析

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

R语言的基本图形

一&#xff0c;条形图 安装包 install.packages("vcd") 绘制简单的条形图 barplot(c(1,2,4,5,6,3)) 水平条形图 barplot(c(1,2,4,5,6,3),horiz TRUE) 堆砌条形图 > d1<-c("Placebo","Treated") > d2<-c("None",&qu…

聚类分析:使用R语言对Iris数据集进行K均值聚类

引言 聚类分析是一种常用的无监督学习技术&#xff0c;旨在将数据集中的样本分成具有相似特征的组。K均值聚类是其中一种常见的方法&#xff0c;它通过将数据点划分为K个簇&#xff0c;并使每个数据点与其所属簇的中心点距离最小化来实现聚类。本文将介绍如何使用R语言执行K均…

matlab求时间序列的时间滞后相关性

matlab求时间序列的时间滞后相关性 自相关、互相关、加权相关、滞后相关等相关性分析&#xff0c;在时间序列分析中经常被用到&#xff0c;可以量化两个时间序列的相关程度&#xff0c;特别对于有季节性趋势的序列中这个分析尤为必要。下面介绍一个Matlab函数&#xff0c;用于进…