阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

集成MyBatis

26次阅读
没有评论

共计 6156 个字符,预计需要花费 16 分钟才能阅读完成。

使用 Hibernate 或 JPA 操作数据库时,这类 ORM 干的主要工作就是把 ResultSet 的每一行变成 Java Bean,或者把 Java Bean 自动转换到 INSERT 或 UPDATE 语句的参数中,从而实现 ORM。

而 ORM 框架之所以知道如何把行数据映射到 Java Bean,是因为我们在 Java Bean 的属性上给了足够的注解作为元数据,ORM 框架获取 Java Bean 的注解后,就知道如何进行双向映射。

那么,ORM 框架是如何跟踪 Java Bean 的修改,以便在 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 异常。这种隐式状态使得普通 Java Bean 的生命周期变得复杂。

此外,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 框架相比,主要有几点差别:

  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.11
  • org.mybatis:mybatis-spring:3.0.0

和前面一样,先创建 DataSource 是必不可少的:

@Configuration
@ComponentScan
@EnableTransactionManagement
@PropertySource("jdbc.properties")
public class AppConfig {@Bean
    DataSource createDataSource() {...}
}

再回顾一下 Hibernate 和 JPA 的 SessionFactoryEntityManagerFactory,MyBatis 与之对应的是 SqlSessionFactorySqlSession

JDBC Hibernate JPA MyBatis
DataSource SessionFactory EntityManagerFactory SqlSessionFactory
Connection Session EntityManager 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 类和 users 表之间映射的 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 接口外,还有 BookMapperBonusMapper……一个一个写太麻烦,因此,MyBatis 提供了一个MapperFactoryBean 来自动创建所有 Mapper 的实现类。可以用一个简单的注解来启用它:

@MapperScan("com.itranswarp.learnjava.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,或者使用数据库的特定语法,但切换数据库可能就不太容易。好消息是大部分项目并没有切换数据库的需求,完全可以针对某个数据库编写尽可能优化的 SQL。

练习

集成 MyBatis 操作数据库。

下载练习

小结

MyBatis 是一个半自动化的 ORM 框架,需要手写 SQL 语句,没有自动加载一对多或多对一关系的功能。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计6156字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中