Spring Boot 数据访问集成 MyBatis 与事物配置
对于软件系统而言,持久化数据到数据库是至关重要的一部分。在 Java 领域,有很多的实现了数据持久化层的工具和框架(ORM)。ORM 框架的本质是简化编程中操作数据库的繁琐性,比如可以根据对象生成 SQL 的 Hibernate ,后面 Hibernate 也实现了JPA 的规范,使用 JPA 的方式只需要几行代码即可实现对数据的访问和操作;MyBatis 的前身是 IBATIS 是一个简化和实现了 Java 数据持久化层的开源框架,相对的不同之处可以灵活调试 SQL , MyBatis 流行的主要原因在于它的简单性和易使用性。
集成 MyBatis
在之前配置 Spring MVC 集成 MyBatis 需要配置文件(SQL写在XML中)、实体类、dao层映射关联等繁琐的配置,后面又开发了 generator ,可以根据表自动生产实体类、配置文件和数据层代码,一定程度上简单了一些编码工作,当然也可以使用注解的方式来配置,简化到现在 Spring Boot 中集成 spring-boot-starter 就可以通过注解的方式直接写 SQL 语句,原则就是约定到大于配置,Spring Boot 要做的就是简化一切。
maven 配置
<!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.2</version> </dependency> <!—jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.1</version> </dependency>
application.properties 配置
#mysql 配置 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://172.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false spring.datasource.username = root spring.datasource.password = 123456 spring.datasource.druid.initial-size=10 spring.datasource.druid.max-active=30 spring.datasource.druid.min-idle=10 spring.datasource.druid.max-wait=10 #spring.datasource.druid.pool-prepared-statements= #spring.datasource.druid.max-pool-prepared-statement-per-connection-size= #spring.datasource.druid.max-open-prepared-statements= #和上面的等价 #spring.datasource.druid.validation-query= #spring.datasource.druid.validation-query-timeout= #spring.datasource.druid.test-on-borrow= #spring.datasource.druid.test-on-return= #spring.datasource.druid.test-while-idle= #spring.datasource.druid.time-between-eviction-runs-millis= #spring.datasource.druid.min-evictable-idle-time-millis= #spring.datasource.druid.max-evictable-idle-time-millis= #spring.datasource.druid.filters= #配置多个英文逗号分隔
上述配置 Spring Boot 会自动加载 spring.datasource.* 相关配置,从而进行 DataSource 对象的配置初始化数据库连接池(这里使用了阿里的 Druid) 。
基于注解的方式的 SQL 配置
实体类
public class Users { private Long id; private String openid; private String username; private Long sex; private java.sql.Date birthday; private String hometown; private String profession; private Long single; private String constellation; private String mobile; private String signture; private java.sql.Timestamp create_time; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Long getSex() { return sex; } public void setSex(Long sex) { this.sex = sex; } public java.sql.Date getBirthday() { return birthday; } public void setBirthday(java.sql.Date birthday) { this.birthday = birthday; } public String getHometown() { return hometown; } public void setHometown(String hometown) { this.hometown = hometown; } public String getProfession() { return profession; } public void setProfession(String profession) { this.profession = profession; } public Long getSingle() { return single; } public void setSingle(Long single) { this.single = single; } public String getConstellation() { return constellation; } public void setConstellation(String constellation) { this.constellation = constellation; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } public String getSignture() { return signture; } public void setSignture(String signture) { this.signture = signture; } public java.sql.Timestamp getCreate_time() { return create_time; } public void setCreate_time(java.sql.Timestamp create_time) { this.create_time = create_time; } }
Mapper
@Mapper public interface UsersMapper { @Select("SELECT id, name, email from users WHERE id=#{id}") Users findUserById(Integer id); /** * 根据openid 获得用户信息 * @param openid * @return */ @Select("SELECT * FROM users WHERE openid=#{openid}") Users getUserByOpenId(String openid); /** * 模糊查询用户信息 * @param username * @return */ @Select("SELECT * FROM users WHERE username LIKE #{username}") List<Users> getUserByUserName(String username); /** * 新增加一个用户 * @param user */ @Insert("INSERT INTO users (openid,username,sex,birthday,hometown,profession,single,constellation,mobile,signture,create_time) VALUES (#{openid},#{username},#{sex},#{birthday},#{hometown},#{profession},#{single},#{constellation},#{mobile},#{signture},#{create_time})") //设置id自增长 @Options(useGeneratedKeys=true,keyColumn="id",keyProperty="id") void insert(Users user); @Update("UPDATE users SET hometown=#{hometown} WHERE id=#{id}") void update(Users user); @Delete("DELETE FROM users WHERE id =#{id}") void delete(Long id); }
接口上定义 @Mapper 的注解,就可以被 Spring 容器扫描到,如果觉得每个接口都需要定义繁琐,也可以在程序的入口点使用 @MapperScan 注解进行 Mapper 的包扫描
@SpringBootApplication @MapperScan("cn.globalrave.barweb.mapper") public class BarWebApplication { public static void main(String[] args) { SpringApplication.run(BarWebApplication.class, args); } }
单元测试
@RunWith(SpringRunner.class) @SpringBootTest public class UserServiceTests { @Autowired private UsersMapper userMapper; @Test public void TestMyBatisUsers() throws Exception { Users user = new Users(); user.setUsername("irving"); user.setOpenid("456798091232322"); user.setHometown("IT 民工"); userMapper.insert(user); Assert.assertEquals(0, userMapper.getUserByUserName(user.getUsername()).size()); } }
Spring 声明式事务管理与 @Transactional 注解使用
事务管理对于程序应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。Spring 对事务管理提供了一致的抽象,为不同的事务API提供一致的编程模型,比如 JDBC, Hibernate,MyBatis, JPA 等,支持基于注解的声明式事务管理,不管是 JPA 还是 JDBC 都实现了 PlatformTransactionManager 接口。比如使用的是 spring-boot-starter-jdbc 依赖,框架会默认注入 DataSourceTransactionManager 实例(MyBatis 依赖于JDBC 所以也是使用的 org.springframework.jdbc.datasource.DataSourceTransactionManager)。使用是 spring-boot-starter-data-jpa 依赖,框架会默认注入 JpaTransactionManager 实例。
可以通过如下代码,查看当前项目使用了哪个实例
@EnableSwagger2Doc @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan @EnableTransactionManagement @MapperScan("cn.globalrave.barweb.mapper") public class BarWebApplication { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Bean public Object getPlatformTransactionManagerBean(PlatformTransactionManager platformTransactionManager){ logger.info(platformTransactionManager.getClass().getName()); return new Object(); } public static void main(String[] args) { SpringApplication.run(BarWebApplication.class, args); } }
声明式事务管理建立在 AOP (依赖aopalliance.jar包)之上的。本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中,这正是 Spring 倡导的非侵入式的开发方式。
配置声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于 @Transactional 注解。
基于tx和aop名字空间的 xml 配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.test.mybatis.mapper" /> <context:property-placeholder location="classpath:jdbc.properties" /> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" p:driverClassName="${jdbc.driver}" p:url="${jdbc.url}" p:username="${jdbc.user}" p:password="${jdbc.password}"/> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="dataSource" p:configLocation="classpath:mybatisConfig.xml" p:mapperLocations="classpath:com/test/mybatis/mapper/*.xml"/> <!-- mybatis sqlSession --> <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean> <!-- 事务管理器配置 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 事务传播配置 --> <tx:advice id="transactionAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="save" propagation="REQUIRED" rollback-for="Exception"/> <tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/> <tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/> <tx:method name="get*" read-only="true" /> </tx:attributes> </tx:advice> <!-- 配置哪些方法参与事务 --> <aop:config proxy-target-class="true"> <aop:pointcut id="transactionxPointcut" expression="execution(*com.test.service.*.*(..)" /> <aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionxPointcut" /> </aop:config> </beans>
让 Spring 管理 MyBatis 中的事务,无需额外配置,只需要设置 MyBatis 中的 org.mybatis.spring.SqlSessionFactoryBean 引用的数据源与 jdbc 中的 DataSourceTransactionManager 引用的数据源一致即可,否则事务管理会不起作用。
Spring 事务的隔离级别
- ISOLATION_DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,另外四个与JDBC的隔离级别相对应。
- ISOLATION_READ_UNCOMMITTED: 这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
- ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据
- ISOLATION_REPEATABLE_READ: 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
- ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
脏读 | 不可重复读 | 幻读 | |
Read uncommitted(未提交读) | √ | √ | √ |
Read committed(已提交读) | × | √ | √ |
Repeatable read(可重复读) | × | × | √ |
Serializable(可串行化) | × | × | × |
脏读(Dirty Read) 脏读意味着一个事务读取了另一个事务未提交的数据,而这个数据是有可能回滚。
不可重复读(Unrepeatable Read) 不可重复读意味着,在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。 例如:事务B中对某个查询执行两次,当第一次执行完时,事务A对其数据进行了修改。事务B中再次查询时,数据发生了改变
幻读(phantom read) 幻读,是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
基于@Transactional 注解配置
基于注解的方式启用事物很简单,使用注解 @EnableTransactionManagement 开启事务支持后,然后在访问数据库的 Service 层上添加注解 @Transactional 便可(被 @Transactional 注解的方法,将支持事务。如果注解在类上,则整个类的所有方法都默认支持事务)。
启动类
@EnableSwagger2Doc @SpringBootApplication // same as @Configuration @EnableAutoConfiguration @ComponentScan @EnableTransactionManagement @MapperScan("cn.globalrave.barweb.mapper") public class BarWebApplication { public static void main(String[] args) { SpringApplication.run(BarWebApplication.class, args); } }
Service 类
@Service public class UsersService implements IUsersService { @Autowired private UsersMapper usersMapper; @Autowired private TagsMapper tagsMapper; @Autowired private ImgsMapper imgsMapper; @Override public Users getUserByOpenId(String openId) { return this.usersMapper.getUserByOpenId(openId); } @Override @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=Exception.class) public Boolean bindWeixinUser(WeixinBindRequest request) { //... } }
如果项目中使用了多个持久化框架(比如 JPA + MyBatis),还可以指定使用哪个事务管理器,SpringBoot 内部提供的事务管理器是根据 autoconfigure 来进行决定的。比如当使用jpa的时候,也就是 pom 中加入了 spring-boot-starter-data-jpa 这个 starter 之后 (之前我们分析过 SpringBoot 的自动化配置原理)。SpringBoot 会构造一个 JpaTransactionManager 这个事务管理器。而当我们使用 spring-boot-starter-jdbc 的时候,构造的事务管理器则是 DataSourceTransactionManager。这2个事务管理器都实现了 Spring 中提供PlatformTransactionManager 接口,这个接口是 Spring 的事务核心接口这个核心接口有以下这几个常用的实现策略:
- HibernateTransactionManager
- DataSourceTransactionManager
- JtaTransactionManager
- JpaTransactionManager
@EnableTransactionManagement @SpringBootApplication public class BarApplication implements TransactionManagementConfigurer { @Resource(name="txManager2") private PlatformTransactionManager txManager2; // 事务管理器1 @Bean(name = "txManager1") public PlatformTransactionManager txManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 事务管理器2 @Bean(name = "txManager2") public PlatformTransactionManager txManager2(EntityManagerFactory factory) { return new JpaTransactionManager(factory); } // 设置默认的事务管理器 @Override public PlatformTransactionManager annotationDrivenTransactionManager() { return txManager2; } public static void main(String[] args) { SpringApplication.run(BarApplication.class, args); } }
使用的时候指定好名称就 OK 了
@Transactional(value="txManager1") public void findUserPassword(String userName) { //get user }
MyBatis 配置打印 SQL 日志
配置日志也很简单,只需要在在 logback 增加如下配置
<!-- mybatis 日志 --> <logger name="cn.globalrave.barweb.mapper" level="DEBUG"/> <logger name="org.mybatis"> <level value="DEBUG"/> </logger>
配置OK后,比如打印出事物的日志
[ INFO ] [115236 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] com.alibaba.druid.pool.DruidDataSource [854] - {dataSource-2} inited [ DEBUG] [115250 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.SqlSessionUtils [97] - Creating a new SqlSession [ DEBUG] [115260 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.SqlSessionUtils [128] - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33ba62e0] [ DEBUG] [115268 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.transaction.SpringManagedTransaction [87] - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@25a7562e] will be managed by Spring [ DEBUG] [115271 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] cn.globalrave.barweb.mapper.ActivityMapper.getActivityById [159] - ==> Preparing: select * from activity where id=? [ DEBUG] [115287 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] cn.globalrave.barweb.mapper.ActivityMapper.getActivityById [159] - ==> Parameters: 0(Integer) [ DEBUG] [115316 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] cn.globalrave.barweb.mapper.ActivityMapper.getActivityById [159] - <== Total: 1 [ DEBUG] [115316 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.SqlSessionUtils [186] - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33ba62e0] [ DEBUG] [115317 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.SqlSessionUtils [284] - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33ba62e0] [ DEBUG] [115317 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.SqlSessionUtils [310] - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33ba62e0] [ DEBUG] [115317 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] org.mybatis.spring.SqlSessionUtils [315] - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@33ba62e0] [ INFO ] [115415 [http-nio-127.0.0.1-5000-exec-1]] [2017-09-03 18:50:25] cn.globalrave.barweb.controller.ActivityController [31] - {"description":"没有嘈杂的喧闹,没有炫目的灯光,更多的是一份安静,一份温暖","end":1503811180000,"id":0,"recommend":"1","start":1503846754000,"subject":" 复古爵士邂逅老上海风情","title":"复古爵士邂逅老上海风情"}
其他组件
上述系统整理了 Spring Boot 整合 MyBatis 的一些配置,实际开发中其实有很多的 GRUD 与分页操作,可以集成 Mybatis-Plus 这个组件
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatisplus-spring-boot-starter</artifactId> <version>1.0.4</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>2.1.0</version> </dependency>
Mapper 继承 BaseMapper 即有了常用的 GRUD 的操作了,是不是很方便。
@Mapper public interface ActivityMapper extends BaseMapper<Activity> { /** * 根据openid 获得用户信息 * @param id * @return */ @Select("select * from activity where id=#{id}") Activity getActivityById(Integer id); }
public interface IActivityService extends IService<Activity> { } @Service public class ActivityServiceImpl extends ServiceImpl<ActivityMapper, Activity> implements IActivityService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired protected ActivityMapper activityMapper; @Transactional(readOnly=true) @Override public Activity getActivityById(int id) { return this.activityMapper.selectById(id); } @Override public List<Activity> getActivityList() { List<Activity> list= this.activityMapper.selectList(new EntityWrapper()); return list; } }
REFER:
https://github.com/baomidou/mybatis-plus
https://github.com/mybatis/spring-boot-starter
http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
连接池多数据源配置
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter分布式事务系列(4.1)Atomikos的分布式案例
https://my.oschina.net/pingpangkuangmo/blog/423210