Spring框架为什么要与持久层技术进行整合?
JavaEE开发需要持久层进行数据库的访问操作。JDBC、Hibernate、MyBatis进行持久开发过程存在大量的代码冗余。Spring基于模板设计模式对于上述的持久层技术进行了封装。Spring可以与哪些持久层技术进行整合?
1. JDBC |- JDBCTemplate 2. Hibernate(JPA) |- HibernateTemplate 3. MyBatis |- SqlSessionFactoryBean MapperScannerConfigure因为MyBatis是目前用得最多的持久层框架,所以这里只讲与MyBatis的整合。
-------七步骤--------
实体实体别名表创建DAO接口实现Mapper文件注册Mapper文件MyBatisAPI调用引入mysql和mybatis的jar包,搭建开发环境
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.3</version> </dependency>创建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> <!-- 设置实体类的别名 --> <typeAliases> </typeAliases> <!-- 数据库连接 --> <environments default="mysql"> <environment id="mysql"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/angenin?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <!-- 注册mapper文件 --> <mappers> </mappers> </configuration>七步骤
创建User实体类 public class User implements Serializable { private Integer id; private String name; private String password; //为了简洁,这里略写 //有参无参 //get/set } 在配置文件中设置User实体类的别名<!-- 设置实体类的别名 --> <typeAliases> <typeAlias alias="user" type="com.angenin1.mybatis.User"/> </typeAliases> 创建表use angenin; create table t_users( `id` int primary key auto_increment, `name` varchar(12), `password` varchar(12) ); desc t_users; 创建UserDAO接口public interface UserDAO { void save(User user); } 实现UserMapper文件<?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.angenin1.mybatis.UserDAO"> <insert id="save" parameterType="user"> insert into t_users(`name`,`password`)values(#{name},#{password}); </insert> </mapper> 在配置文件中注册Mapper文件<!-- 注册mapper文件 --> <mappers> <mapper resource="UserMapper.xml"/> </mappers> MyBatisAPI调用(测试)public class TestMyBatis { public static void main(String[] args) throws IOException { // 读取主配置文件 InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); // 获取sqlSession工厂对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); // 获取sqlSession对象 SqlSession session = sqlSessionFactory.openSession(); // 获取UserDAO对象 UserDAO userDAO = session.getMapper(UserDAO.class); // 创建用户 User user = new User(); user.setName("angenin"); user.setPassword("123456"); // 保存用户 userDAO.save(user); // 提交事务 session.commit(); } } 运行结果:
搭建环境
<!-- mybatis --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.3</version> </dependency> <!--除了上面导入的mybatis和mysql的jar包外,还需要导入三个jar包--> <!-- spring整合mybatis --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.14.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.18</version> </dependency>Spring配置文件的配置
<!-- druid 连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/angenin?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <!-- 通过 SqlSessionFactoryBean 创建SqlSessionFactory对象 --> <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--设置连接池对象--> <property name="dataSource" ref="dataSource"/> <!--指定实体类所在的包--> <property name="typeAliasesPackage" value="com.angenin1.entity"/> <!--指定mapper文件所在的包--> <property name="mapperLocations"> <list> <value>classpath:com.angenin1.mapper/*Mapper.xml</value> </list> </property> </bean> <!-- 通过 MapperScannerConfigurer 创建DAO对象 --> <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--设置sqlsession工厂对象--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/> <!--指定DAO接口所在的包--> <property name="basePackage" value="com.angenin1.dao"/> </bean>编码
-------七步骤--------
实体实体别名表创建DAO接口实现Mapper文件注册Mapper文件MyBatisAPI调用七步变四步
实体表创建DAO接口实现Mapper文件 创建entity包,在其包下创建User实体类public class User implements Serializable { private Integer id; private String name; private String password; //get set } 创建t_users表# 上面已经创建了,继续用t_users表 create table t_users( `id` int primary key auto_increment, `name` varchar(12), `password` varchar(12) ); 创建dao包,在其包下创建UserDAO接口public interface UserDAO { void save(User user); } 在resources目录下创建com.angenin1.mapper目录(注意:resources目录下多级目录用/分隔,这里用.,所以不是多级目录,只是一个目录),用于存放mapper文件,在这个目录下创建UserMapper.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.angenin1.dao.UserDAO"> <insert id="save" parameterType="User"> insert into t_users(`name`,`password`)values(#{name},#{password}); </insert> </mapper> 测试public class TestMyBatisSpring { @Test public void test() { ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext5.xml"); UserDAO userDAO = (UserDAO) ctx.getBean("userDAO"); User user = new User(); user.setName("angenin"); user.setPassword("11"); userDAO.save(user); } } 运行结果:问题:Spring与Mybatis整合后,为什么DAO不提交事务,但是数据能够插入数据库中?
谁创建的Connection,谁就控制着事务(tx)。 本质上控制连接对象(Connection)--> 连接池(DataSource) 之前由MyBatis提供的连接池对象 --> 创建Connection Connection.setAutoCommit(false) 需要我们手动控制事务,操作完成后手工调用commit进行提交。 而现在由Druid(C3P0、DBCP)作为连接池 --> 创建Connection Connection.setAutoCommit(true) 默认true,保持着自动控制事务,每条sql提交一次。答案:因为Spring与MyBatis整合时,引入了外部连接池对象,保存自动的事务提交这个机制(Connection.setAutoCommit(true)),不需要手工进行事务的操作,也能进行事务的提交。
注意:未来实战中,还会手工控制事务(多条sql一起成功,一起失败),后续Spring通过事务控制解决这个问题。
保证业务操作完整性的一种数据库机制。
事务的4个特点:A C I D A:原子性 C:一致性 I:隔离性 D:持久性 口诀:原子一致才能隔离持久。(个人口诀,不喜勿喷( ̄▽ ̄)")
结论:控制事务的底层都是Connection对象完成的。
Spring是通过AOP的方式进行事务开发的。
要注意的细节:
原始对象 --> 原始方法 --> 核心功能(业务处理+DAO调用)因为Service依赖DAO对象,所以需要把DAO作为Service的成员变量,并提供get/set方法,用依赖注入(set注入)的方式进行赋值。而Spring在也帮我们封装了额外功能的这些内容,我们只要调用org.springframework.jdbc.datasource.DataSourceTransactionManager,而由于需要Connection对象,需要等于依赖,依赖就要注入,所以需要注入Connection对象,而又由于Connection对象来着连接池,所以注入连接池即可。
所以以后我们只需要:
调用org.springframework.jdbc.datasource.DataSourceTransactionManager注入连接池 <!-- druid 连接池 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> ... </bean> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>使用DataSourceTransactionManager后,切入点由@Transactional注解决定。
@Transactional:事务的额外功能加入给哪些业务方法。
加在类上:这个类中的所有方法都会有额外功能,即加入事务。加在方法上:这个方法会有额外功能,即加入事务。切入点 + 额外功能
<!-- transaction-manager:组装额外功能bean --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>组装切入点:会自动扫描@Transactional注解。
搭建开发环境
<!--引入Spring关于事务的jar包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.1.14.RELEASE</version> </dependency>原始对象 创建service包,然后新建UserService和UserServiceImpl类
public interface UserService { void register(User user); } public class UserServiceImpl implements UserService { private UserDAO userDAO; // 原始方法 @Override public void register(User user) { // 核心功能 userDAO.save(user); } public UserDAO getUserDAO() { return userDAO; } public void setUserDAO(UserDAO userDAO) { this.userDAO = userDAO; } } <bean id="userService" class="com.angenin.service.UserServiceImpl"> <!-- 这里的userDAO是前面MapperScannerConfigurer创建的,创建的bean的id为首单词首字母小写 --> <property name="userDAO" ref="userDAO"/> </bean>额外功能
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>切入点 在UserServiceImpl类上加上@Transactional注解,把整个类作为切入点。
组装切面
<!-- transaction-manager:组装额外功能bean --> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>测试
@Test public void test02() { ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext5.xml"); UserService userService = (UserService) ctx.getBean("userService"); User user = new User(); user.setName("angenin11"); user.setPassword("123456"); userService.register(user); }细节
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="true"/> proxy-target-class属性:进行动态代理底层实现的切换 false:JDK代理(默认) true:Cglib代理属性:描述物体特征的一系列值。如一个人的属性有身高、体重、性别等。 事务属性:描述事务特征的一系列值,共5个。
隔离属性传播属性只读属性超时属性异常属性添加事务属性: @Transactional(isolation = xxx, propagation = xxx, readOnly = xxx, timeout = xxx, rollbackFor = xxx, noRollbackFor = xxx)
隔离属性:描述了事务解决并发问题的特征。
什么是并发? 多个事务(用户)在同一时间访问操作了相同的数据。 同一时间:0.000几秒的差异,有 微小的前 和 微小的后 之分。
并发会产生哪些问题?
脏读不可重复读幻读并发问题如何解决? 通过隔离属性解决,隔离属性中设置不同的值,解决并发处理过程中的问题。
脏读 一个事务,读取了另一个事务中还没提交的数据,会在本事务中产生数据不一致的问题。
解决方案:@Transactional(isolation = Isolation.READ_COMMITTED) Isolation.READ_COMMITTED:只能读取已提交的数据。
不可重复读 一个事务,多次读取相同的数据,但读取的结果不一样(可能是别的事务修改了这条数据),会在本事务中产生数据不一致的问题。 注意:1. 不是脏读(读取的是已提交的数据);2. 一个事务中。
解决方案:@Transactional(isolation = Isolation.REPEATABLE_READ Isolation.REPEATABLE_READ:数据库底层会对操作的数据加上行锁。
幻读 一个事务,多次整表进行查询统计(不是单条数据,是一个结果集),但结果不一样(可能是别的事务增加或删除了数据),会在本事务中产生数据不一致的问题。
解决方案:@Transactional(isolation = Isolation.SERIALIZABLE Isolation.SERIALIZABLE:数据库底层会对操作的数据加上表锁。
总结:
并发安全:SERIALIZABLE > REPEATABLE_READ > READ_COMMITTED运行效率:READ_COMMITTED > REPEATABLE_READ > SERIALIZABLEIsolation共有五个值:
DEFAULT (默认)READ_UNCOMMITTED (读未提交)READ_COMMITTED (读已提交)REPEATABLE_READ(可重复读)SERIALIZABLE(串行化)Oracle采用多版本比对的方式解决不可重复读的问题。
默认的隔离属性:Isolation.ISOLATION_DEFAULT(会调用不同数据库所设置的默认隔离属性)
MySQL:REPEATABLE_READ(解决不可重复读,加行锁) Oracle:READ_COMMITTED(解决脏读,不加锁)
查看数据库隔离级别: MySQL5:select @@tx_isolation; MySQL8:select @@transaction_isolation; Oracle: 电脑没装Oracle,所以这里直接截图。
建议:在实战中,使用默认值即可。
未来的实战中,并发访问情况很少,因为需要海量的数据,如果真遇到并发问题,使用乐观锁来解决,不会太影响效率。 乐观锁: Hibernate(JPA):支持乐观锁,使用Version MyBatis:不支持乐观锁,所以需要通过拦截器自定义开发。 MyBatis-plus:支持乐观锁,使用version
传播属性:描述了事务解决嵌套问题的特征。 事务嵌套:一个大事务中,包含多个小事务。(service调用service时会发生事务嵌套) 问题:大事务中融入了很多小的事务,他们彼此影响,最终会导致外部大的事务丧失原子性。
只读属性:针对于只进行查询操作的业务方法,可以加入只读属性,提高运行效率(不会加锁)。
默认为false,查询操作改为true,只读。
超时属性:指定了事务等待的最长时间(以秒为单位)。 访问的数据被其他事务加上锁,此时需要等待解锁。
默认为-1,最终由对应的数据库底层设置的超时时间来决定等待多久,一般不用去设置超时属性,使用默认值即可。
Spring事务处理过程中
对应RuntimeException及其子类,默认采用回滚策略。对应Exception及其子类,默认采用提交策略。设置成回滚:rollbackFor = {java.lang.Exception.class, xxx, xxx} 设置成不回滚(即提交):{java.lang.RuntimeException.class, xxx, xxx}
建议:实战中使用默认值即可,异常尽量使用RuntimeException及其子类。
使用建议:
增删改操作:@Transactional查询操作:@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)两种方法的差异在于第3步和第4步,第2步需要多配置一个事务属性。
基于标签的事务配置 1. 原始类 ... 2. 额外功能与事务属性 ... <tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager"> <tx:attributes> <tx:method name="register"/> <tx:method name="login" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice> <!-- 等效于 @Transactional public void register(User user) {...} @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public void login(String username, String password) {...} --> <aop:config> 3. 切入点 <aop:pointcut id="pc" expression="execution(* com.angenin1.service.UserServiceImpl.*(..))"/> 4. 组装切面 <aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/> </aop:config>事务属性配置
<tx:advice id="txAdvice" transaction-manager="dataSourceTransactionManager"> <tx:attributes> <!--<tx:method name="register"/>--> <!--<tx:method name="login" propagation="SUPPORTS" read-only="true"/>--> <!--使用通配符解决tx:method标签过多的问题--> <!--编程时,service中负责增删改操作的方法都以modify开头即可--> <tx:method name="modify*"/> <!-- *:指除上面的其他方法(查询),需要把范围小的放前面,范围大的放后面 --> <tx:method name="*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice>切入点
<aop:config> <!--<aop:pointcut id="pc" expression="execution(* com.angenin1.service.UserServiceImpl.*(..))"/>--> <!--使用包切入点--> <aop:pointcut id="pc" expression="execution(* com.angenin1.service..*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/> </aop:config>下一篇:Spring5学习笔记(五、MVC框架整合)
学习视频(p108-p140):https://www.bilibili.com/video/BV185411477k?p=108
