上一章我们讲到,如果说hibernate是面向对象为主,关系为辅,那么在mybatis中则是着重考虑的是关系模型,换句话说,如果对象模型设计的不好,就会很容易的感觉到实现的难度。
首先来看看最简单的单向many2one:
建立对象:
public class Customer {
private Long id;
private String name;
}
public class Orders {
private Long id;
private String sn;
private Double price;
private Customer customer;
}
很简单,Orders和Customer是一个单向的many2one的关系。和hibernate一样,我们先来配置简单的customer:
创建CustomerMapper.xml:
<mapper namespace="cd.itcast.mybatis.customer">
<insert id="save" keyProperty="id" parameterType="Customer"
useGeneratedKeys="true">
INSERT INTO customer(name) values (#{name})
</insert>
<select id="get" resultType="Customer" parameterType="int">
SELECT * FROM customer WHERE id = #{id}
</select>
</mapper>
只是一个很简单的单对象映射操作。
在hibernate的many2one中我们知道,在many方的外键关联到one方的主键。在many方保存的时候,应该在对应外键保存关联one方的id。所以,many方的保存insert应该是这样:
OrdersMapper.xml:
<insert id="save" keyProperty="id" parameterType="Orders"
useGeneratedKeys="true">
INSERT INTO orders(sn,price,customer_id) values (#{sn},#{price},#{customer.id})
</insert>
在这里看到,我们在对应外键(customer_id)的地方,使用#{customer.id}来代表orders的customer属性的id属
性,mybatis会像EL那样自动帮我们得到值。所以,在这里一定要注意保存对象的顺序,必须是先保存one再保存many方。在hibernate
中,如果保存顺序有误,hibernate会在提交事务的时候同步脏数据,帮我们补一条update语句来保证对象的关系正确,但是在mybaits中,
所有的sql都由我们来完成,所以没人会帮我们来完成update。所以,对应的保存测试应该是:
@Test
public void testSave(){
SqlSession session=MyBatisUtil.openSession();
Customer c=new Customer();
c.setName("itcasT");
Orders o=new Orders();
o.setPrice(800d);
o.setSn("001");
o.setCustomer(c);
session.insert("cd.itcast.mybatis.customer.save",c);
session.insert("cd.itcast.mybatis.orders.save",o);
session.commit();
session.close();
}
下面就是many方的获取了。如果只是简单的把orders配置成一个单对象,即使用resultType来映射:
<select id="get" parameterType="long" resultType="Orders">
SELECT * FROM orders WHERE o.id = #{id}
</select>
如果是这样,在测试的时候:
@Test
public void testGet(){
SqlSession session=MyBatisUtil.openSession();
Orders o=session.selectOne("cd.itcast.mybatis.orders.get", 1l);
System.out.println(o);
Customer c=o.getCustomer();
System.out.println(c);
session.close();
}
会看到,orders对应的customer是null,只能说明mybatis不能帮我们自动完成相关的关系映射。下面就来看下在mybatis中的关系映射:
第一种方式:使用额外的sql:
<select id="get" parameterType="int" resultMap="ordersmap">
SELECT * FROM orders WHERE o.id = #{id}
</select>
在这里,添加resultMap来完成相关的映射:
<resultMap type="Orders" id="ordersmap">
<id property="id" column="id"/>
<result property="sn" column="sn"/>
<result property="price" column="price"/>
<association property="customer" column="customer_id" javaType="Customer"
select="cd.itcast.mybatis.customer.get">
</resultMap>
可能第一次看这个配置会觉得很奇怪,其实很好理解:
1,association:在mybatis中不像hibernate那样,把关系分的很细致,比如one-to-one,many-to-one,而
只是把关系分成了两种:第一种是对单对象的关系,就用association,一种是集合的关系,这个待会再看。因为这里orders只是把
customer对象作为自己的一个属性,所以使用association来表示这个关系;
2,property:和hibernate一样,定义这个关系在orders对象上面的对应属性。在完成关联对象的映射后,会使用这个属性把对象设置到orders对象上;
3,column:代表从查询结果集(select * from orders where id = ?)中得到对关系维护的列,这里就指明的是外键customer_id;
4,javaType:代表完成映射后应该返回的对象类型,这里肯定就是Orders
5,select:是这种映射方式的最关键的地方,这个select指代着cd.itcast.mybatis.customer.get这条SQL,这
条sql即是得到customer的sql,而这条sql需要传入一个customer的id,我们前面已经通过
column="customer_id"指定了应该传给cd.itcast.mybatis.customer.get的值就是结果集中的
customer_id。
所以,可以看出,mybatis在这种映射的方式为:首先执行SELECT * FROM orders WHERE o.id = #{id},然后对
id,sn,price属性直接从结果集中得到;接着,从结果集中得到customer_id列对应的值,这个值即是orders对应customer对
象的id,然后把这个值作为id交给cd.itcast.mybatis.customer.get去执行,而这个执行的结果会被拼装为一个
Customer对象,接着这个对象再会被设置到orders的customer属性中。完成映射。
再次执行get测试:
DEBUG [main] - ooo Using Connection [com.mysql.jdbc.JDBC4Connection@19b1de]
DEBUG [main] - ==> Preparing: SELECT * FROM ORDERS WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, sn, cust_id
TRACE [main] - <== Row: 1, 001, 1
DEBUG [main] - ooo Using Connection [com.mysql.jdbc.JDBC4Connection@19b1de]
DEBUG [main] - ==> Preparing: SELECT * FROM CUSTOMER WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, name
TRACE [main] - <== Row: 1, c
Orders [id=1, sn=001]
Customer [id=1, name=c]
可以看到,正常查询得到结果。但是,可能会有点疑问,我们之前在学hibernate的时候,不是说延迟加载是一种比较好的处理对象的方式么?但是现在我们看到例子中的对象并没有延迟加载。因为默认情况下,mybatis不会延迟加载对象:
要开启延迟加载,需要给mybatis增加几个配置,在mybatis-config.xml中:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
再次执行get测试:得到的结果和刚才一样,仍然感觉没有执行延迟加载。问题在哪里呢?其实mybatis已经完成了延迟加载,只是mybatis的延迟
加载策略和hibernate的延迟加载策略不一样。Hibernate是把被加载的目标对象做成proxy对象提供延迟加载(即这里的
Customer),而mybatis是把对象本身做成延迟加载对象,我们把get测试修改一下:
SqlSession session = MyBatisUtil.getInstance().openSession();
OrdersMapper om=session.getMapper(OrdersMapper.class);
Orders o=om.get(1l);
System.out.println(o.getClass());
session.close();
打印结果:
DEBUG [main] - ooo Using Connection [com.mysql.jdbc.JDBC4Connection@19b1de]
DEBUG [main] - ==> Preparing: SELECT * FROM ORDERS WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, sn, cust_id
TRACE [main] - <== Row: 1, 001, 1
class cd.itcast.mybatis.domain.Orders$$EnhancerByCGLIB$$39a00050
001
可以看到,orders对象却是是proxy对象,并且访问其他属性,不会发送sql。但是,只要得到customer,那么查询customer的
sql就会立刻发送。简单来说,mybatis的延迟加载原则是,只要你去拿这个对象,表明你在用这个对象。注意和hibernate的区别。
第二种方式,也是mybatis推荐的方式。上面那种方式,容易产生N+1问题。所以,mybatis建议使用内联的映射方式。修改ordersmapping:
<resultMap type="Orders" id="ordersmapper">
<id property="id" column="id"/>
<result property="sn" column="sn"/>
<association property="customer" javaType="Customer">
<id property="id" column="cid"/>
<result property="name" column="cname"/>
</association>
</resultMap>
<select id="get" resultMap="ordersmapper" parameterType="long">
SELECT c.id as cid,c.name as cname,o.* FROM
ORDERS o LEFT JOIN CUSTOMER c ON o.CUST_ID = o.id WHERE o.id = #{id}
</select>
第一,在select中使用left join的方式把orders和对应的customer一起查询出来,并且为customer设置对应的别名。
第二,修改resultMap,去掉select和column,而修改为内部的映射形式。
那么,当mybatis执行完成sql之后,会把响应的属性转换成Orders对象的属性,当解析到customer属性时,mybatis会使用内部的映射关系把结果集中对应的列转换成Customer对象。
再次运行get,执行结果相同。
再次说明,mybatis建议使用第二种方式来完成映射。有人又会考虑到,那么如果只是orders的下拉列表,在结果里面不需要customer,这条
sql不是很浪费性能么?这里又要说到mybatis的另一个原则,按需配置。意思是,如果只是用作下拉列表,那么只需要再做一个select来专门为
orders的下拉列表做筛选。