2022-06-14:Spring(7):集成Mybatis
使用Hibernate或JPA操作数据库时,这类ORM干的主要工作是把ResultSet的每一行变为JavaBean,或者把Java Bean自动转换为INSERT或UPDATE语句的参数中,从而实现ORM。
而ORM框架之所以知道如何把行数据映射到JavaBean,是因为我们在JavaBean的属性上给了足够的注解作为元数据,ORM框架获取Java Bean的注解后,就知道如何进行双向映射。
那么ORM是如何跟踪JavaBean的修改,以便在update()操作中更新必要的属性?
答案是使用Proxy模式,从ORM框架读取的User实例实际上并不是User类,而是代理类,代理类继承自User类,但针对每个setter方法都做了覆写:
public class UserProxy extends User { boolean _isNameChanged; public void setName(String name) { super.setName(name); _isNameChanged = true; } }
这样,代理类就可以跟踪到每个属性的变化。
针对一对一或多对一关系时,代理类可以直接通过getter方法查询数据库:
public class UserProxy extends User { Session _session; boolean _isNameChanged; public void setName(String name) { super.setName(name); _isNameChanged = true; } /** * 获取User对象关联的Address对象: */ public Address getAddress() { Query q = _session.createQuery("from Address where userId = :userId"); q.setParameter("userId", this.getId()); List<Address> list = query.list(); return list.isEmpty() ? null : list(0); } }
为了实现这样的查询,UserProxy必须保存Hibernate的当前Session。但是当事务提交后,Session自动关闭,此时再获取getAddress()将无法访问数据库,或者获取到的不是事务一致的数据。因此,ORM框架总是引入了Attached/Detached状态,表示当前此Java Bean到底是在Session范围内,还是脱离了Session变为了一个“游离”对象。如果我们无法正确理解状态变化与事务边界,就会造成大量的PersistentObjectException异常。这种隐式状态使得普通的JavaBean生命周期变得复杂。
此外,Hibernate和JPA为了实现兼容多个数据库,它使用HQL或JPQL查询,经过一道转换,变为特定数据库的SQL,理论上这样可以做到无缝切换数据库,但是这一层自动转换除了少许的性能开销外,给SQL级别的优化带来了麻烦。
最后,ORM框架通常提供了缓存,并且还分为一级和二级缓存。
- 一级缓存:在一个Session范围内的缓存,常见的场景是根据主键查询时,两次查询可以返回同一实例:
User user1 = session.load(User.class,123); User user2 = session.load(User.class,123);
- 二级缓存:跨Session的缓存,一般默认关闭,需要手动配置。二级缓存极大地增加了数据不一致性,原因在于SQL非常灵活,常常会导致意外的更新:
// 线程1读取: User user1 = session1.load(User.class, 123); ... // 一段时间后,线程2读取: User user2 = session2.load(User.class, 123);
当二级缓存生效时,两个线程读取到的User实例其实是一样的,但是数据库对应的行记录完全可能被修改,例如:
-- 给老用户增加100积分:
UPDATE users SET bonus = bonus + 100 WHERE createdAt <= ?
ORM无法判断id=123的用户是否接受该UPDATE语句影响。考虑到数据库通常会支持多个应用程序,此UPDATE语句可能由其他进程执行,ORM框架就更不知道了。
我们把这种ORM框架称之为全自动ORM框架。
对比Spring提供的JdbcTemplate,它和ORM框架相比,主要有几点差别:
- 查询后需要手动提供Mapper实例以便把ResultSet的每一行变为Java对象;
- 增删改操作所需的参数列表,需要手动传入,即把User实例变为[user.id , user.name , user.email]这样的列表,比较麻烦。
但是JdbcTemplate的优势在于其确定性,即每次读取操作一定是数据库操作而非缓存,所执行的SQL是完全确定的,缺点在于代码繁琐,构造INSERT INTO users VALUES (?,?,?)更加复杂。
所以,介于全自动ORM如Hibernate与手写全部如JdbcTemplate之间,还有一种半自动ORM,它只负责把ResultSet自动映射为Java Bean,或者自动填充Java Bean参数,但仍需自己写出SQL。MyBatis就是这样一种半自动化ORM框架。
我们来看看如何在Spring中集成MyBatis。
首先我们要引入MyBatis本身,其次,由于Spring并没有像Hibernate那样内置对MyBatis的集成,所以,我们需要再引入MyBatis官方自己开发的一个与Spring集成的库:
- org.mybatis:mybatis:3.5.4
- org.mybatis:mybatis-spring:2.0.4
和前边一样,先创建DataSource是必不可少的:
@Configuration @ComponenetScan @EnableTransactionManagement @PropertySource("jdbc.properties") public class AppConfig{ @Bean DataSource createDataSource(){ ... } }
再回顾一下Hibernate与JPA的SessionFactory与EntityManagerFactory,Mybatis与之对应的SqlSessionFactory与SqlSession:
可见,ORM的设计套路都是类似的。使用MyBatis的核心就是创建SqlSessionFactory,这里我们需要创建的是SqlSessionFactoryBean:
@Bean SqlSessionFactoryBean createSqlSessionFactoryBean(@Autowired DataSource dataSource){ var sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean; }
因为MyBatis可以直接使用Spring管理的声明式事务,因此,创建事务管理器和使用JDBC是一样的:
@Bean PlatformTransactionManager createTxManager(@Autowired DataSource dataSource){ return new DataSourceTransactionManager(dataSource); }
和Hibernate不同的是,MyBatis使用Mapper来实现映射,而且Mapper必须是接口。我们以User类为例,在User类与user表之间映射的UserMapper编写如下:
public interface UserMapper{ @Select("SELECT * FROM users WHERE id = #{id}") User getById(@Param("id") long id); }
注意这里的Mapper并非JdbcTemplate的RowMapper的概念,它是定义访问users表的接口方法。比如我们定义了一个User getById(long)的主键查询方法,不仅要定义接口方法本身,还要明确写出查询的SQL,这里用注解@Select标记。SQL语句的任何参数,都与方法参数按名称对应。例如,方法参数id的名字通过注解@Param()标记为id,则SQL语句中将来替换的占位符就是#{id}。
如果有多个参数,那么每个参数命名后直接在SQL中写入对应的占位符即可:
@Select("SELECT * FROM users LIMIT #{offset} , #{maxResults}") List<User> getAll(@Param("offset") int offset , @Param("maxResults") int maxResults);
注意:MyBatis执行查询后,将根据方法的返回类型自动把ResultSet的每一行转换为User实例,转换规则为按列名与属性名对相应。如果列名与属性名不同,最简单的方法是编写SELECT语句的别名:
-- 列名是created_time,属性名是createdAt:
SELECT id, name, email, created_time AS createdAt FROM users
执行INSERT语句就稍微麻烦一点,因为我们希望传入User实例,因此,定义的方法接口与@Insert注解如下:
@Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})") void insert(@Param("user") User user);
上述方法传入的参数名称为user,参数类型为User类,在SQL中引用的时候,以#{obj.property}的方式写占位符。和Hibernate这样的全自动化ORM相比,MyBatis必须写出完整的INSERT语句。
如果users表的id是自增主键,那么,我们再SQL中不传入id,但希望获取插入后的主键,需要再加一个@Options注解:
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id") @Insert("INSERT INTO users (email, password, name, createdAt) VALUES (#{user.email}, #{user.password}, #{user.name}, #{user.createdAt})") void insert(@Param("user") User user);
keyProperty与keyColumn分别指出JavaBean的属性与数据库主键列名。
执行UPDATE与DELETE语句相对比较简单,我们定义方法如下:
@Update("UPDATE users SET name = #{user.name}, createdAt = #{user.createdAt} WHERE id = #{user.id}") void update(@Param("user") User user); @Delete("DELETE FROM users WHERE id = #{id}") void deleteById(@Param("id") long id);
有了UserMapper接口,还需要对应的实现类才能真正执行这些数据库操作的方法。虽然可以自己写实现类,但我们除了编写UserMapper接口外,还有BookMapper、BonusMapper....一个一个写太麻烦了。因此MyBatis提供了一个MapperFactoryBean来自动创建所有Mapper的实现类。可以用一个简单的注解来启用它:
@MapperScan("com.itranswarp.learn.java.mapper") ...其他注解... public class AppConfig{ ... }
有了@MapperScan,就可以让MyBatis自动扫描指定包的所有Mapper并创建实现类。在真正的业务逻辑中,我们可以直接注入:
@Component @Transactional public class UserService { // 注入UserMapper: @Autowired UserMapper userMapper; public User getUserById(long id) { // 调用Mapper方法: User user = userMapper.getById(id); if (user == null) { throw new RuntimeException("User not found by id."); } return user; } }
可见,业务逻辑主要是通过XxxMapper定义的数据库方法来访问数据库。
XML配置
上述在Spring中集成MyBatis的方式,我们只需要用到注解,并没有任何XML配置文件。MyBatis也允许使用XML配置映射关系与SQL语句,例如,更新User时根据属性值构造动态SQL:
<update id="updateUser"> UPDATE users SET <set> <if test="user.name != null"> name = #{user.name} </if> <if test="user.hobby != null"> hobby = #{user.hobby} </if> <if test="user.summary != null"> summary = #{user.summary} </if> </set> WHERE id = #{user.id} </update>
编写XML配置的优点是可以组装出动态SQL,并且把所有SQL操作集中在一起。缺点是配置起来太繁琐,调用方法时如果还想查看SQL需要定位到XML配置中。这里我们就不再介绍XML的配置方式,有需要的可以自行阅读官方文档。
使用MyBatis最大的问题是所有SQL都需要手写,优点是执行的SQL就是我们自己写的SQL,优化很简单,也可以编写复杂的SQL,或者是数据库的特定语法,但是切换数据库可能不太容易。好消息是,大部分项目没有切换数据库的需求,完全可以针对某个数据库编写尽可能优化的SQL。
小结
MyBatis是一个半自动化的ORM框架,需要手写SQL语句,没有自动加载一对多或多对一关系的功能。