mybatis使用入门、springboot整合,以及整合后的事务管理原理
mybatis是一款支持自定义SQL、存储过程和高级映射的持久化框架。通过封装几乎消除了使用者编写JDBC、手动设置参数和检索结果的代码,其底层实现通过XML配置文件、Java注解的方式来配置,将Mapper接口和POJO类映射到数据库
第一步:配置mybatis的运行环境
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="db.properties" /> <settings> <setting name="logImpl" value="LOG4J"/> <setting name="mapUnderscoreToCamelCase" value="true" /> </settings> <!--包别名,便于在mapper xml文件中简写resultType--> <typeAliases> <package name="com.luhc.mybatis.simple.model" /> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="UNPOOLED"> <property name="url" value="${db.connectionURL}"/> <property name="driver" value="${db.driverClass}"/> <property name="username" value="${db.userName}"/> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <mappers> <!-- 配置Mapper接口 --> <mapper resource="com/luhc/mybatis/simple/mapper/UserMapper.xml"/> </mappers> </configuration>
第二步:根据配置文件来创建SqlSessionFactoryBuilder、创建SqlsessionFactory、SqlSession、从SqlSession中获取Mapper接口代理类对象
@Test public void testXMLConfig() throws IOException { SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<User> users = userMapper.findAll(); Assert.assertFalse(users.isEmpty()); User user = userMapper.findByKey(1L); Assert.assertNotNull(user); }
@Test public void testCodeConfig() throws IOException { // 从properties文件中加载配置 Properties properties = new Properties(); properties.load(Resources.getResourceAsStream("db.properties")); Properties configProperties = new Properties(); String connectUrl = properties.getProperty("db.connectionURL"); String driverClass = properties.getProperty("db.driverClass"); String userName = properties.getProperty("db.userName"); String password = properties.getProperty("db.password"); configProperties.setProperty("url", connectUrl); configProperties.setProperty("driver", driverClass); configProperties.setProperty("username", userName); configProperties.setProperty("password", password); // 配置数据源 UnpooledDataSourceFactory unpooledDataSourceFactory = new UnpooledDataSourceFactory(); unpooledDataSourceFactory.setProperties(configProperties); DataSource dataSource = unpooledDataSourceFactory.getDataSource(); // 配置事务工厂 TransactionFactory transactionFactory = new JdbcTransactionFactory(); // 配置Enviroment对象 Environment environment = new Environment("development", transactionFactory, dataSource); // 配置Configuration对象 Configuration configuration = new Configuration(environment); // 配置mapper configuration.addMapper(UserMapper.class); // 创建SqlSessionFactory实例,用于获取SqlSession SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); // 创建SqlSession实例,用于获取Mapper接口的代理实例 SqlSession sqlSession = sqlSessionFactory.openSession(); // 执行Mapper接口的方法,进行实际的查询 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<User> users = userMapper.findAll(); Assert.assertFalse(users.isEmpty()); User user = userMapper.findByKey(1L); Assert.assertNotNull(user); }
定义XML文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.luhc.mybatis.simple.mapper.UserMapper"> <!--id对应接口中的方法名; resultType为映射的POJO类--> <select id="findAll" resultType="com.luhc.mybatis.simple.model.User"> select * from user </select> <select id="findByKey" resultType="com.luhc.mybatis.simple.model.User"> select * from user where id = #{id} </select> </mapper>
定义接口:类名与XML中的namespace一致,方法名与XML中的id一致
public interface UserMapper { List<User> findAll(); User findByKey(Long id); }
public interface UserMapper { @Select("select * from user") List<User> findAll(); @Select("select * from user where id = #{id}") User findByKey(Long id); }
environment
实现多数据源,通过在配置中定义多个environment实例,在创建SqlSessionFactory的时候,传入指定的environment来实现多数据源并存的功能
类型转换
用于实现Jdbc数据类型Java类型之间的相互转换,通过实现接口TypeHandler来实现,mybatis提供了抽象类BaseTypeHandler来方便我们实现该接口。可参考文档 typeHandler
插件
用于拦截mybatis底层代码执行的机制,可参考文档 plugin 。 PageHelper
官方文档
mybatis-spring是一个使mybatis和spring无缝集成的库,由mybatis社区开发。主要作用:
在spring中配置DataSource
@Configuration @PropertySource({"classpath:/db.properties"}) public class DataSourceConfig { @Value("${db.connectionURL}") private String jdbcUrl; @Value("${db.driverClass}") private String driverClass; @Value("${db.userName}") private String userName; @Value("${db.password}") private String password; @Bean public DataSource masterDataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUrl(jdbcUrl); druidDataSource.setDriverClassName(driverClass); druidDataSource.setUsername(userName); druidDataSource.setPassword(password); return druidDataSource; } }
定义对应的Mapper接口和POJO模型类
@Mapper public interface CountryMapper { @Delete({ "delete from country", "where id = #{id,jdbcType=INTEGER}" }) int deleteByPrimaryKey(Integer id); } @Data public class Country { // ... }
在spring中配置SqlSessionFactory
@Configuration @MapperScan("com.luhc.mapper") // 扫描 public class MybatisConfig { @Bean @Autowired public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
在直接使用mybatis的场景中,SqlSessionFactory通过SqlSessionFactoryBuilder来构建,在mybatis-spring集成环境中通过SqlSessionFactoryBean来构建,这是一个实现了spring接口FactoryBean、InitializingBean的类。在应用启动时会创建一个SqlSessionFactory的实例,并以sqlSessionFactory保存在spring IOC容器中。
解开这个问题需要回到SqlSessionFactoryBean在应用启动时创建SqlSessionFactory的过程
可以看出,经过这一个流程后,返回的SqlSessionFactory实现类为DefaultSqlSessionFactory,可以通过它获取其内部的SpringManagedTransactionFactory。
我们再来看看在DefaultSqlSessionFactory中是如何创建SqlSession的。
public class DefaultSqlSessionFactory implements SqlSessionFactory { @Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); // 调用的是SpringManagedTransactionFactory#newTransaction方法 tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); // 获取到的SqlSession return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } }
通过DefaultSqlSessionFactory创建的SqlSession为DefaultSqlSession,其内部包含了Executor实例(内部包含了上一步中TransactionFactory创建的Transaction对象),追踪到DefaultSqlSession内部
public class DefaultSqlSession implements SqlSession { @Override public void commit(boolean force) { try { // 委托给Executor实例来进行实际的提交 executor.commit(isCommitOrRollbackRequired(force)); dirty = false; } catch (Exception e) { throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } }
追踪到Executor内部、其实现类有SimpleExecutor(一般使用这个)、BatchExecutor(批量处理)、ReuseExecutor(?)
public abstract class BaseExecutor implements Executor { @Override public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); flushStatements(); if (required) { // 委托给Transaction实例处理 transaction.commit(); } } }
到了这一步,对应的Transaction实例为SpringManagedTransaction
public class SpringManagedTransaction implements Transaction { @Override public Connection getConnection() throws SQLException { if (this.connection == null) { openConnection(); } return this.connection; } private void openConnection() throws SQLException { // 用Spring的DataSourceUtils工具类获取链接 this.connection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.connection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource); if (LOGGER.isDebugEnabled()) { LOGGER.debug( "JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring"); } } @Override public void commit() throws SQLException { if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Committing JDBC Connection [" + this.connection + "]"); } this.connection.commit(); } } }
SqlSessionTemplate利用动态代理生成SqlSession接口的代理对象,通过SqlSessionInterceptor拦截了方法执行,在获取SqlSession的时候委托给Spring的TransactionSynchronizationManager#getResource方法来获取(底层为ThreadLocal原理实现)
提供了便利的实现方式,让直接使用SqlSession的数据访问层可以使用SqlSessionTemplate来保证线程安全和委托spring事务管理
第一种,手动注册
@Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource()); return (SqlSessionFactory) sqlSessionFactory.getObject(); } @Bean public UserMapper userMapper() throws Exception { SqlSessionTemplate sessionTemplate = new SqlSessionTemplate(sqlSessionFactory()); return sessionTemplate.getMapper(UserMapper.class); }
第二种,自动扫描
使用@MapperScan注解,查看注解源码,发现可以指定要使用的SqlSessionFactory bean的名称
@Configuration @MapperScan("com.luhc.mapper") public class MybatisConfig { @Bean @Autowired public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } @Bean @Autowired public PlatformTransactionManager dataSourceTransactionManager(DataSource dataSource) { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }
注册一个MapperScannerConfigurer实例
MapperScannerConfigurer实现了接口BeanDefinitionRegistryPostProcessor,在实例化时,spring框架会回调方法MapperScannerConfigurer#postProcessBeanDefinitionRegistry,进行mapper接口的扫描
mybatis-spring-boot-starter为了快速在springboot中集成mybatis,使用这个库,我们将消除模板式的xml配置代码,更少的XML配置。
直接引入依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency>
定义mapper接口
@Mapper public interface CityMapper { @Select("SELECT * FROM CITY WHERE state = #{state}") City findByState(@Param("state") String state); }
springboot启动类
@SpringBootApplication public class SampleMybatisApplication implements CommandLineRunner { private final CityMapper cityMapper; public SampleMybatisApplication(CityMapper cityMapper) { this.cityMapper = cityMapper; } public static void main(String[] args) { SpringApplication.run(SampleMybatisApplication.class, args); } @Override public void run(String... args) throws Exception { System.out.println(this.cityMapper.findByState("CA")); } }
引入依赖后,应用启动后将自动将DataSource实例注入并创建SqlSessionFactory、自动创建SqlSessionTemplate实例、自动扫描mapper接口并为其关联SqlSessionTemplate实例(保证SqlSession线程安全)。其内部默认会扫描到@Mapper注解标记的接口,如果需要更改这个机制,需要使用@MapperScan注解来配置
通过简单的XML配置,可以根据数据库表生成POJO映射类、XML mapper文件、Java mapper接口。而且对XML mapper文件不会覆盖掉自定义过的内容,只是写入自动生成有变动的部分,对于Java mapper接口文件则会直接覆盖。
这里提供一个在maven运行插件的方式,包含了常用配置的配置样例, 更多的配置详情请自行到官网挖掘
maven插件配置
<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.7</version> <configuration> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <executions> <execution> <id>Generate MyBatis Artifacts</id> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!--生成lombok形式的java bean--> <dependency> <groupId>com.softwareloop</groupId> <artifactId>mybatis-generator-lombok-plugin</artifactId> <version>1.0</version> </dependency> </dependencies> </plugin>
generatorConfig.xml配置
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!--指定外部.properties配置文件--> <properties resource="db.properties" /> <!--defaultModelType=flat,设置一个表只生成一个java实体类--> <context id="MySQL" targetRuntime="MyBatis3" defaultModelType="flat"> <!--定义分隔符--> <property name="autoDelimitKeywordss" value="true" /> <property name="beginningDelimiter" value="`" /> <property name="endingDelimiter" value="`" /> <property name="javaFileEncoding" value="UTF-8" /> <!--生成lombok形式的java bean--> <plugin type="com.softwareloop.mybatis.generator.plugins.LombokPlugin" /> <!--注释管理器--> <commentGenerator> <!--阻止生成注释--> <!--<property name="suppressAllComments" value="true" />--> <!--阻止生成的注释信息包含时间戳--> <property name="suppressDate" value="true" /> <!--注释带上数据库表的注释信息--> <property name="addRemarkComments" value="true" /> </commentGenerator> <!--jdbc连接器--> <jdbcConnection connectionURL="${db.connectionURL}" driverClass="${db.driverClass}" userId="${db.userName}" password="${db.password}"> </jdbcConnection> <!--jdbc类型与java类型转换器--> <javaTypeResolver > <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <javaModelGenerator targetPackage="com.luhc.mybatis.simple.model" targetProject="src/main/java"> <!--不根据database划分不同的包--> <property name="enableSubPackages" value="false" /> <!--从数据库查询的值不必去除空格--> <property name="trimStrings" value="false" /> </javaModelGenerator> <sqlMapGenerator targetPackage="com.luhc.mybatis.simple.mapper" targetProject="src/main/resources"> <!--不根据database划分不同的包--> <property name="enableSubPackages" value="false" /> </sqlMapGenerator> <javaClientGenerator type="ANNOTATEDMAPPER" targetPackage="com.luhc.mybatis.simple.mapper" targetProject="src/main/java"> <!--不根据database划分不同的包--> <property name="enableSubPackages" value="false" /> </javaClientGenerator> <table schema="mybatis" tableName="country" > <!--参考配置说明http://www.mybatis.org/generator/configreference/generatedKey.html--> <generatedKey column="id" sqlStatement="SELECT LAST_INSERT_ID()" type="post" identity="true"/> </table> <!-- 生成多张表 --> <table schema="mybatis" tableName="user"> <!--参考配置说明http://www.mybatis.org/generator/configreference/generatedKey.html--> <generatedKey column="id" sqlStatement="SELECT LAST_INSERT_ID()" type="post" identity="true"/> <!-- 指定该字段对应的Java类型 --> <columnOverride column="sex" property="sex" javaType="com.luhc.mybatis.simple.model.SexEnum" /> </table> </context> </generatorConfiguration>
预定义了基本的单表CRUD操作方法,减少了手动编写CRUD的工作。
引入依赖
<dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency>
maven插件配置,mybatis generator代码生成器
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.6</version> <configuration> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> <executions> <execution> <id>Generate MyBatis Artifacts</id> <goals> <goal>generate</goal> </goals> </execution> </executions> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.6</version> </dependency> <!--生成lombok形式的java bean--> <dependency> <groupId>com.softwareloop</groupId> <artifactId>mybatis-generator-lombok-plugin</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper</artifactId> <version>4.1.5</version> </dependency> </dependencies> </plugin> </plugins> </build>
generatorConfig配置
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!--defaultModelType=flat,设置一个表只生成一个java实体类--> <context id="MySQL" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <!--定义分隔符--> <property name="autoDelimitKeywordss" value="true" /> <property name="beginningDelimiter" value="`" /> <property name="endingDelimiter" value="`" /> <property name="javaFileEncoding" value="UTF-8" /> <!--生成lombok形式的java bean。与通用Mapper插件冲突--> <!--<plugin type="com.softwareloop.mybatis.generator.plugins.LombokPlugin" />--> <!--通用mapper插件--> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <!--生成的Mapper会继承这里指定的类--> <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/> <property name="caseSensitive" value="true"/> <property name="forceAnnotation" value="true"/> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <property name="needsData" value="true" /> </plugin> <!--注释管理器--> <commentGenerator> <!--阻止生成注释--> <!--<property name="suppressAllComments" value="true" />--> <!--阻止生成的注释信息包含时间戳--> <property name="suppressDate" value="true" /> <!--注释带上数据库表的注释信息--> <property name="addRemarkComments" value="true" /> </commentGenerator> <!--jdbc连接器--> <jdbcConnection connectionURL="jdbc:mysql://111.230.14.116:3306/mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false" driverClass="com.mysql.cj.jdbc.Driver" userId="用户" password="密码"> </jdbcConnection> <!--jdbc类型与java类型转换器--> <javaTypeResolver > <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <javaModelGenerator targetPackage="com.treeinsea.glimmer.model" targetProject="src/main/java"> <!--不根据database划分不同的包--> <property name="enableSubPackages" value="false" /> <!--从数据库查询的值不必去除空格--> <property name="trimStrings" value="false" /> </javaModelGenerator> <sqlMapGenerator targetPackage="com.treeinsea.glimmer.mapper" targetProject="src/main/resources"> <!--不根据database划分不同的包--> <property name="enableSubPackages" value="false" /> </sqlMapGenerator> <javaClientGenerator type="XMLMAPPER" targetPackage="com.treeinsea.glimmer.mapper" targetProject="src/main/java"> <!--不根据database划分不同的包--> <property name="enableSubPackages" value="false" /> </javaClientGenerator> <!--catalog指定表所在的数据库,避免了同名表的问题--> <table tableName="country" catalog="mybatis" > <property name="ignoreQualifiersAtRuntime" value="true" /> <!--参考配置说明http://www.mybatis.org/generator/configreference/generatedKey.html--> <generatedKey column="id" sqlStatement="SELECT LAST_INSERT_ID()" type="post" identity="true"/> </table> <table tableName="user" catalog="mybatis" > <property name="ignoreQualifiersAtRuntime" value="true" /> <!--参考配置说明http://www.mybatis.org/generator/configreference/generatedKey.html--> <generatedKey column="id" sqlStatement="SELECT LAST_INSERT_ID()" type="post" identity="true"/> </table> </context> </generatorConfiguration>
application.yaml指定扫描的基接口
mapper: mappers: - tk.mybatis.mapper.common.Mapper not-empty: true