3.2 对象关系映射

ORM(Object Relational Mapping),对象关系映射,是一种为了解决面向对象与关系型数据库不匹配而出现的技术,使开发者能够用面向对象的方式使用关系型数据库。

目前最为常用的ORM框架主要是MyBatis(前身是ibatis)和Hibernate。两者对比如下:

  1. MyBatis非常简单易学,Hibernate相对较复杂,门槛较高。

  2. 相比起Hibernate,MyBatis灵活性更好。

  3. 系统数据处理量巨大,性能要求极为苛刻的情况下,MyBatis能够高度定制化SQL,因此会有更好的可控性和表现。

  4. MyBatis需要手写SQL语句,也可以生成一部分,Hibernate则基本上可以自动生成,偶尔会写一些HQL。同样的需求,MyBatis的工作量比Hibernate要大很多。类似的,如果涉及到数据库字段的修改,Hibernate修改的地方很少,而MyBatis要把那些SQL mapping的地方一一修改。

  5. 以数据库字段一一对应映射得到的PO和Hibernte这种对象化映射得到的PO是截然不同的,本质区别在于这种PO是扁平化的,不像Hibernate映射的PO是可以表达立体的对象继承,聚合等等关系的,这将会直接影响到你的整个软件系统的设计思路。

Hibernate现在主要用在传统企业应用的开发,互联网领域中由于流量大、并发高的缘故因此主要以MyBatis为主。笔者也推荐使用MyBatis做为ORM框架。

一般的ORM包括以下几个部分:   

  • 一个规定Mapping Metadata的工具, 即数据库中的表、列与对象以及对象属性的映射。   

  • 一个对持久类对象进行CRUD操作的API。

  • 一个语言或API用来规定与类和类属性相关的查询。   

  • 一种技术可以让ORM的实现同事务对象一起进行缓存、延迟加载等操作。

3.2.1 Mapping

MyBatis支持XML配置:

<!--mybatis-config.xml-->
<?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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="TestUserMapper.xml"/>
  </mappers>
</configuration>

<!--TestUserMapper.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="me.rowkey.pje.mybatis.TestUserMapper">
  <select id="selectUser" resultType="TestUser">
    select * from test_user where id = #{id}
  </select>
</mapper>

其中,environment元素体中包含了事务管理和连接池的配置,能够根据不同的环境使用不同的数据库配置。这里的dataSource设置为POOLED时使用了MyBatis自己提供的数据连接池。mappers元素则是包含一组mapper映射器(这些 mapper 的 XML 文件包含了 SQL 代码和映射定义信息)。

MyBatis也支持注解映射Mapper:

public interface TestUserMapper {
  
  @Select("SELECT * FROM test_user WHERE id = #{id}")
  TestUser selectUser(int id);

}

这里需要注意的是,命名空间现在是必需的。并且MyBatis对所有的命名配置元素的解析如果是全限定名则直接用;而如果是一个简单的名称,全局唯一的话没有问题,如果有重复类则会报错。

此外,MyBatis提供了mybatis-spring这个类库用于集成Spring和MyBatis。配置如下:

<!-- datasoure数据源  -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
    <property name="username" value="${mysql.username}"></property>
    <property name="password" value="${mysql.pwd}"></property>
    <property name="maxTotal" value="${mysql.max}"></property>
    <property name="minIdle" value="${mysql.minIdle}"></property>
    <property name="maxIdle" value="${mysql.maxIdle}"></property>
    ...
</bean>	
 
<!-- 配置sqlSessionFactory工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">  
	<property name="dataSource" ref="dataSource" />  
	<property name="configLocation" value="classpath:sqlMapConfig.xml"></property>  
</bean>

<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
	<constructor-arg index="0"  ref="sqlSessionFactory" />
</bean>

<bean id="userDAO" class="me.rowkey.pje.mybatis.dao.UserDao">
	<property name="sqlSessionTemplate" ref="sqlSessionTemplate"></property>
</bean>

3.2.2 CRUD以及属性的查询

MyBatis的核心是SqlSessionFactory,要使用它最基本的操作API,首先获取到SqlSessionFactory,然后再去获取相应的Mapper。

String resource = "classpath:mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
TestUserMapper mapper = session.getMapper(TestUserMapper.class);
TestUser testUser = mapper.selectUser(1);
...

此外,在Spring中可以使用MyBatis提供的org.mybatis.spring.SqlSessionTemplate进行操作。

SqlSessionTemplate sqlTemplate = new SqlSessionTemplate(sqlSessionFactory);
TestUser testUser = (TestUser)this.sqlSessionTemplate.selectOne("me.rowkey.pje.mybatis.TestMapper.selecUser", uid);

这里需要注意的是,SqlSession是非线程安全的,而SqlSessionTemplate则是线程安全的。

MyBatis对应于CRUD有以下几个映射语句:

  • insert映射插入语句

  • update映射更新语句

  • delete映射删除语句

  • select映射查询语句

依赖于上述的四种操作,可以完成各种crud以及对属性之类的查询。MyBatis会自动把数据库查询结果注入到返回对象中。

这里需要注意的是上述的select * from test_user where id = #{id},其中的#{id}是告诉MyBatis创建预处理语句属性并以它为背景设置安全的值, 对应于PreparedStatement中的?。此外,在MyBatis中还存在另一个符号$,如下:

<select id="selectUserByStatus" resultType="TestUser">
    select * from test_user where status = #{status} order by ${columnName}
</select>

$在这里的作用只是做字符串替换,不会修改或转义字符串。因此,能使用#的地方就不要用$,除非是像order by这种不是参数的地方。

此外,MyBatis对于insert、update、delete以及select都提供了很多选项,用来支持注入缓存、自动映射、列名和属性名的转换等优化功能。

3.2.3 缓存

MyBatis支持一级缓存和二级缓存:

  • Mybatis默认开启一级缓存,是SqlSession级别的,Session结束那么缓存即清空。另外还支持Statement级别,即缓存只对当前的Statement有效(没什么用)。

  • MyBatis的二级缓存是Mapper级别的,被多个SqlSession共享,需要手动开启。

<!--TestMapper.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="me.rowkey.pje.mybatis.TestMapper">
    <cache/>
    <select id="selectUser" resultType="TestUser" useCache="false" flushCache="true">
        select * from test_user where id = #{id}
    </select>
</mapper>

上述配置中的useCache设置为true即开启二级缓存,对应mapper中的所有select语句的结果集将会被缓存。也可以针对某个select设置useCache为false关闭二级缓存,设置flushCache为true使得数据直接flush到数据库中防止出现脏读数据。此外,二级缓存也可以配置成使用自定义的缓存实现。

<cache type="me.rowkey.pje.mybatis.cache.MyCustomCache"/>

需要注意的是MyBatis中的缓存设计初衷是针对单点应用的,都是本地缓存,现在的分布式应用慎重开启,只单纯把MyBastis作为一个ORM框架,在Service层自己实现缓存机制是更好的选择。

3.2.4 结果映射

MyBatis默认会自动映射查询结果,根据SQL返回的列名并在Java类中查找相同名字的属性(忽略大小写)。还可以全局配置对数据库列的LowUnderscore(a_column)命名转换为LowCamel命名(aColumn)。

<configuration>  
     <settings>  
          <setting name="mapUnderscoreToCamelCase" value="true" />  
     </settings>  
</configuration>  

除此之外,MyBatis还支持自定义映射。

public class UserDto{
    private long uid;
    private String userName;
    
    //getter and setters
    ....
}

<mapper namespace="me.rowkey.pje.mybatis">  
    <resultMap type="UserDto" id="userResultMap">  
       <result property="userName" column="uname"/>  
    </resultMap> 
    
    <!-- 根据uid查询用户开放信息 -->  
    <select id="getOpenUserInfo" parameterType="String" resultType="UserDto" resultMap="userResultMap"
        useCache="true" flushCache="true">  
        <![CDATA[  
            select uid,uname from test_user  
                WHERE uid = #{id}   
        ]]>    
    </select>   
</mapper>

上面使用的resultMap即做了自定义的映射工作,uid会自动映射, userName会使用自定义映射。

3.2.5 SQL语句构建器

虽然Mybatis已经做了很多封装,可以大大简化SQL编写的工作,但是很多时候在代码拼接SQL是无法避免的。如果用字符串自己进行拼接,那么各种+号、括号、引号、格式化问题会带来很多的麻烦,一不小心就会出错。针对这种状况,MyBatis提供了SQL语句构建类org.apache.ibatis.jdbc.SQL, 可以大大简化动态SQL编写的问题。

new SQL() {{
    SELECT("user.id, user.user_name");
    SELECT("user.sign, user.gender");
    FROM("user_account user");
    INNER_JOIN("user_base_info uinfo on user.uid = uinfo.uid");
    WHERE("user.user_name like ?");
    OR();
    WHERE("uinfo.nick_name like ?");
    ORDER_BY("user.create_time");
  }}.toString();

等同于

"SELECT user.id, user.user_name, "
"user.sign, user.gender " +
"FROM user_account user " +
"INNER JOIN user_base_info uinfo on user.uid = uinfo.uid" +
"WHERE (user.user_name like ?) " +
"OR (uinfo.nick_name like ?) " +
"ORDER BY user.create_time";

3.2.6 使用提示

  1. 当几个SQL语句都包含同样的部分SQL逻辑时。可以使用<include, refid="id" />来进行复用,如:

    <sql id="getTestByStatus_fragment">
        from test_meta where status = #{status} 
    </sql>
    
    <select id='getTestList' parameterType='map' resultMap='Test'>
        select id
        <include refid="getTestByStatus_fragment"/>
        order by
        create_time desc
    </select>
  2. 动态SQL中的空字符串判断

    在动态SQL中判断是否是空字符串时,MyBatis的内建机制不太好用。建议使用以下方式:

    <if test="param != null and param != '' ">
  3. 多ResultMap复用

    一个应用中由于功能的不同经常会有多个mapper.xml,如果一个文件需要另一个文件中的resultMap的定义,可以直接引用,而不需要再重新定义一遍。如:

    <!--A.mapper.xml-->
    <mapper namespace="me.rowkey.pje.mybatis">
        ...
        <resultMap id="userResultMap" type="me.rowkey.pje.mybatis.UserDto"></resultMap>
    </mapper>
    
    <!--B.mapper.xml-->
    <mapper namespace="me.rowkey.pje.test">
        ...
        <select id="bSql" parameterType="map" resultMap="me.rowkey.pje.mybatis.userResultMap>
        ...
        </select>
    </mapper>

Last updated