四、mybatis的一些特殊功能 special
除了简化数据库编程外,MyBatis还提供了各种功能,这些对实现一些常用任务非常有用,比如按页加载表数据,存取CLOB/BLOB类型的数据,处理枚举类型值,等等。
4.1 处理枚举类型
MyBatis支持持久化enum类型属性。假设t_user表中有一列gender(性别)类型为 varchar2(10),存储 MALE 或者 FEMALE 两种值。并且,Student对象有一个enum类型的gender 属性,如下所示:
public enum Gender {
FEMALE,MALE
}
默认情况下MyBatis使用EnumTypeHandler来处理enum类型的Java属性,并且将其存储为 enum值的名称。你不需要为此做任何额外的配置。你可以可以向使用基本数据类型属性一样使用enum类型属性,如下:
drop table t_user;
create table t_user(
id number primary key,
name varchar2(50),
gender varchar2(10)
);
public class User{
private Integer id;
private String name;
private Gender gender;
//setters and getters
}
<insert id="insertUser" parameterType="User">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert into t_user(id,name,gender)
values(#{id},#{name},#{gender})
</insert>
当你执行insertStudent语句的时候MyBatis会取Gender枚举(FEMALE/MALE)的名称,然后将其存储到GENDER列中。如果你希望存储原enum的顺序位置(0/1),而不是enum名,你需要明确地配置它
如果你想存储FEMALE为0,MALE为1到gender列中,你需要在mybatis-config.xml文件中配置EnumOrdinalTypeHandler:
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.briup.special.Gender"/>
注意:使用顺序位置为值存储到数据库时要当心。顺序值是根据enum中的声明顺序赋值的。如果你改变了Gender里面对象的声明顺序,则数据库存储的数据和此顺序值就不匹配了。
4.2 处理CLOB/BLOB类型数据
BLOB和CLOB都是大字段类型,BLOB是按二进制来存储的,而CLOB是可以直接存储文字的。通常像图片、文件、音乐等信息就用BLOB字段来存储,先将文件转为二进制再存储进去。而像文章或者是较长的文字,就用CLOB存储.
BLOB和CLOB在不同的数据库中对应的类型也不一样:
MySQL 中:clob对应text/longtext,blob对应blob
Oracle中:clob对应clob,blob对应blob
MyBatis提供了内建的对CLOB/BLOB类型列的映射处理支持。
drop table user_pics;
create table user_pics(
id number primary key,
name varchar2(50) ,
pic blob,
bio clob
);
这里,照片可以是PNG,JPG或其他格式的。简介信息可以是学生或者讲师的漫长的人生经历。默认情况下,My Batis将CLOB类型的列映射到java.lang.String类型上、而把BLOB列映射到byte[]类型上。
public class UserPic{
private int id;
private String name;
private byte[] pic;
private String bio;
//setters & getters
}
<insert id="insertUserPic" parameterType="UserPic">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert into user_pics(id,name, pic,bio)
values(#{id},#{name},#{pic},#{bio})
</insert>
<select id="getUserPicById" parameterType="int" resultType="UserPic">
select * from user_pics where id=#{id}
</select>
java代码:
@Test
public void test_insertUserPic(){
byte[] pic = null;
try {
//读取用户头像图片
File file = new File("src/com/briup/special/test.png");
InputStream is = new FileInputStream(file);
pic = new byte[is.available()];
is.read(pic);
is.close();
} catch (Exception e){
e.printStackTrace();
}
String name = "tom";
String bio = "可以是很长的字符串";
//准备好要插入到数据库中的数据并封装成对象
UserPic userPic = new UserPic(name, pic , bio);
SqlSession session = null;
try{
session = MyBatisSqlSessionFactory.openSession();
SpecialMapper mapper = session.getMapper(SpecialMapper.class);
mapper.insertUserPic(userPic);
session.commit();
}catch (Exception e) {
e.printStackTrace();
session.rollback();
}finally {
if(session!=null)session.close();
}
}
下面的getUserPic()方法展示了怎样将CLOB类型数据读取到String类型,BLOB类型数据读取成byte[]属性:
@Test
public void test_getUserPicById(){
SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();
SpecialMapper mapper = session.getMapper(SpecialMapper.class);
UserPic userPic = mapper.getUserPicById(24);
System.out.println(userPic.getId());
System.out.println(userPic.getName());
System.out.println(userPic.getBio());
System.out.println(userPic.getPic().length);
} catch (Exception e) {
e.printStackTrace();
}finally {
if(session!=null)session.close();
}
}
4.3 传入多个输入参数
MyBatis中的映射语句有一个parameterType属性来制定输入参数的类型。如果我们想给映射语句传入多个参数的话,我们可以将所有的输入参数放到HashMap中,将HashMap传递给映射语句。同时MyBatis还提供了另外一种传递多个输入参数给映射语句的方法。假设我们想通过给定的name和email信息查找学生信息,定义查询接口如下:
对于映射器中的方法,MyBatis默认从左到右给方法的参数命名为param1、param2…,依次类推。
public interface StudentMapper{
List<Student> findAllStudentsByNameEmail(String name, String email);
}
MyBatis支持将多个输入参数传递给映射语句,并以#{param}的语法形式引用它们:
<select id="findAllStudentsByNameEmail" resultMap="StudentResult">
select stud_id, name,email, phone from Students
where name=#{param1} and email=#{param2}
</select>
这里#{param1}引用第一个参数name,而#{param2}引用了第二个参数email。
代码中调用:
Student Mapper student Mapper = sql Session.get Mapper(StudentMapper.class);
student Mapper.findAllStudentsByNameEmail(name, email);
4.4 多行结果集映射成Map
可以使用之前我们介绍到的接口的方式来实现(默认把列名作为key,列中的值作为value)。
如果有一些特殊的情况,比如需要使用id值作为key,把一行数据封装成的对象作为value放到map中的话,需要使用下面的方式:
<select id="findAllUsers" resultType="User">
select id,name,gender from t_user
</select>
Map<Integer, User> map = session.selectMap("com.briup.mappers.SpecialMapper.findAllUsers","id");
for(Integer key:map.keySet()){
System.out.println(key+" : "+map.get(key));
}
注意:需要注意gender列的值都是数字还是都是字符串(需要一致)
这里map将会将id作为key值,而每行数据封装成的User对象作为value值。
4.5 使用RowBounds对结果集进行分页
有时候,我们会需要跟海量的数据打交道,比如一个有数百万条数据级别的表。由于计算机内存的现实我们不可能一次性加载这么多数据,我们可以获取到数据的一部分。特别是在Web应用程序中,分页机制被用来以一页一页的形式展示海量的数据。
MyBatis可以使用RowBounds逐页加载表数据。RowBounds对象可以使用offset和limit参数来构建。参数offset表示开始位置,而limit表示要取的记录的数目
<select id="findAllUsers" resultType="User">
select id,name,gender from t_user
</select>
public List<User> findAllUsers(RowBounds rowBounds);
然后,你可以加载第一页数据(前5条):
int offset = 0;
int limit = 5;
RowBounds rowBounds = new RowBounds(offset, limit);
List<Student> = studentMapper.getStudents(rowBounds);
若要展示第二页,使用offset=5,limit=5
但是其实Mybatis的分页是基于内存的分页(查出所有记录再按偏移量和limit取结果),在大数据量的情况下这样的分页效率会很低。
oracle使用rownum也可以完成分页:
rownum 只能等于1
rownum 大于0
rownum 可以小于任何数
例如:把sql语句查询结果当做一张表再查询
select *
from (
select rownum as rowno, t.*
from t_user t
where rownum <= 10
) temp
where temp.rowno >= 5;
4.6 使用ResultHandler自定义结果集ResultSet处理
MyBatis在将查询结果集映射到java对象方面提供了很大的选择性。但是,有时候我们会遇到由于特定的目的,需要我们自己处理SQL查询结果的情况。MyBatis提供了ResultHandler接口,可以让我们以任何自己喜欢的方式处理结果集ResultSet。
例如:我们要把t_user表中所有数据的id和name查询出来,并且把id值作为key,把name值作为value封装到Map集合中
注意:sqlSession.selectMap()则可以返回以给定列为key,记录对象为value的map。但是不能将其配置成使用其中一个属性作为key,而另外的属性作为 value。但是mybatis在之后的版本中可能会完成这个功能
对于sqlSession.select()方法,我们可以传递给它一个ResultHandler接口的实现,它会被调用来处理ResultSet的每一条记录,而且完成我们上面的需求:
@Test
public void test_ResultHandler(){
final Map<Integer,String> map = new HashMap<Integer, String>();
SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();
session.select("com.briup.mappers.SpecialMapper.findAllUsers", new ResultHandler<User>() {
@Override
public void handleResult(ResultContext<? extends User> resultContext) {
User user = resultContext.getResultObject();
map.put(user.getId(), user.getName());
}
});
for(Integer key:map.keySet()){
System.out.println(key+" : "+map.get(key));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(session!=null)session.close();
}
}
在上述的代码中,我们提供了匿名内部类对ResultHandler接口的实现,在handleResult()方法中,我们使用context.getResultObject()获取当前的result对象,即User对象,并对查询返回的每一行都会调用handleResult()方法,从而我们从User对象中取出id和name的值,将其放到map中。
4.7 缓存
将从数据库中加载的数据缓存到内存中,是很多应用程序为了提高性能而采取的一贯做法。默认情况下,mybatis会启用一级缓存;即,如果你使用同一个SqlSession接口对象调用了相同的SELECT语句,则直接会从缓存中返回结果,而不是再查询一次数据库。
注意:session调用commit或close方法后,一级缓存就会被清空
例如: 根据日志输出可以看出,下面代码只会发出一条sql查询语句
@Test
public void test_cache1(){
SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();
SpecialMapper mapper = session.getMapper(SpecialMapper.class);
User user1 = mapper.findUserById(21);
System.out.println(user1);
// session.commit();
User user2 = mapper.findUserById(21);
System.out.println(user2);
} catch (Exception e) {
e.printStackTrace();
}finally {
if(session!=null)session.close();
}
}
二级缓存: 在不同的session对象之间可以共享缓存数据
1.mybatis-config.xml文件中保证设置中是缓存功能是开启的,默认就是开启的true
2.在需要二级缓存的xml映射文件中,手动开启缓存功能,在根元素中加入一个标签即可:
3.一个session查询完数据之后,需要调用commit或者close方法后,这个数据才会进入到缓存中,然后其他session就可以共享到这个缓存数据了
注意:默认情况下,被二级缓存保存的对象需要实现序列化接口,可以通过cache标签的readOnly属性进行设置
例如:
mybatis-config.xml:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
xml映射文件:
<mapper namespace="com.briup.mappers.SpecialMapper">
<cache/>
<select> ..</select>
<select> ..</select>
<select> ..</select>
</mapper>
测试代码:
@Test
public void test_cache2(){
SqlSession session1 = null;
SqlSession session2 = null;
try {
session1 = MyBatisSqlSessionFactory.openSession();
session2 = MyBatisSqlSessionFactory.openSession();
SpecialMapper mapper1 = session1.getMapper(SpecialMapper.class);
SpecialMapper mapper2 = session2.getMapper(SpecialMapper.class);
User user1 = mapper1.findUserById(21);
System.out.println(user1);
session1.commit();
User user2 = mapper2.findUserById(21);
System.out.println(user2);
session2.commit();
} catch (Exception e) {
e.printStackTrace();
}finally {
if(session1!=null)session1.close();
if(session2!=null)session2.close();
}
}
二级缓存补充说明
1. 映射语句文件中的所有select语句将会被缓存
2. 映射语句文件中的所有insert,update和delete语句会刷新缓存
3. 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。
4. 缓存会根据指定的时间间隔来刷新。
5. 缓存会存储1024个对象
cache标签常用属性:
<cache
eviction="FIFO" <!--回收策略为先进先出-->
flushInterval="60000" <!--自动刷新时间60s-->
size="512" <!--最多缓存512个引用对象-->
readOnly="true"/> <!--true表示对象不能被写出去,即不可以被序列化,false表示可以写出去,即可以被序列化,默认值是false-->