2022-06-14:Spring(7):集成Mybatis

使用Hibernate或JPA操作数据库时,这类ORM干的主要工作是把ResultSet的每一行变为JavaBean,或者把Java Bean自动转换INSERTUPDATE语句的参数中,从而实现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为了实现兼容多个数据库,它使用HQLJPQL查询,经过一道转换,变为特定数据库的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框架相比,主要有几点差别:

  1. 查询后需要手动提供Mapper实例以便把ResultSet每一行变为Java对象
  2. 增删改操作所需的参数列表,需要手动传入,即把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与之对应的SqlSessionFactorySqlSession:

 

 

 可见,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并非JdbcTemplateRowMapper的概念,它是定义访问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);

keyPropertykeyColumn分别指出JavaBean的属性数据库主键列名

执行UPDATEDELETE语句相对比较简单,我们定义方法如下:

@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语句,没有自动加载一对多多对一关系的功能。

 

posted @ 2022-06-14 11:07  ShineLe  阅读(14)  评论(0)    收藏  举报