mybatis知识点(四)

3.6 动态SQL  dynamic-sql
有时候,静态的SQL语句并不能满足应用程序的需求。我们可以根据一些条件,来动态地构建 SQL语句。
例如,在Web应用程序中,有可能有一些搜索界面,需要输入一个或多个选项,然后根据这些已选择的条件去执行检索操作。在实现这种类型的搜索功能,我们可能需要根据这些条件来构建动态的SQL语句。如果用户提供了任何输入条件,我们需要将那个条件添加到SQL语句的WHERE子句中。MyBatis通过使用,,,,元素提供了对构造动态SQL语句的高级别支持。


DynamicSql
3.6.1 If 条件
元素被用来有条件地嵌入SQL片段,如果测试条件被赋值为true,则相应地SQL片段将会被添加到SQL语句中。假定我们有一个课程搜索界面,设置了讲师(Tutor)下拉列表框,课程名称(CourseName)文本输入框,开始时间(StartDate)输入框,结束时间(EndDate)输入框,作为搜索条件。假定课讲师下拉列表是必须选的,其他的都是可选的。
当用户点击搜索按钮时,我们需要显示符合以下条件的成列表:
特定讲师的课程 
课程名
包含输入的课程名称关键字的课程;如果课程名称输入为空,则取所有课程
在开始时间和结束时间段内的课程
我们可以对应的映射语句,如下所示:


 
  
  
  
  
  
 


 


public interface DynamicSqlMapper{ 
List searchCourses(Map map); 

public void searchCourses(){ 
Map map = new HashMap(); 
map.put("tutorId", 1); 
map.put("courseName", "%Java%"); 
map.put("startDate", new Date()); 
DynamicSqlMapper mapper = sqlSession.getMapper(DynamicSqlMapper.class); 
List courses = mapper.searchCourses(map);
for (Course course : courses){ 
System.out.println(course); 


此处将生成查询语句SELECT * FROM COURSES WHERE TUTOR_ID= ? AND NAME like ? AND START_DATE >= ?。



3.6.2 choose,when 和 otherwise 条件
有时候,查询功能是以查询类别为基础的。首先,用户需要先选择是否希望通过选择讲师,课程名称,开始时间,或结束时间作为查询条件类别来进行查询,然后根据选择的查询类别,输入相应的参数。在这样的情景中,我们【需要只使用其中一种】查询类别。
MyBatis提供了元素支持此类型的SQL预处理。 
如果没有选择查询类别,则查询开始时间在今天之后的课程,代码如下:
注意:mysql中now()是当前时间 oracle需要使用sysdate

MyBatis计算测试条件的值,且使用第一个值为TRUE的子句。如果没有条件为 true,则使用内的子句。
相当于我们常用的java代码中的这个例子:
if(){
..
}
else if(){
..
}
else{
..
}
 测试例子:public void test_when(){ 
try {
SqlSession sqlSession=
MyBatisSqlSessionFactory.openSession();
Map map = new HashMap(); 
map.put("tutorId", 1); 
map.put("courseName", "%Java%"); 
//输入根据什么类型来查询
map.put("searchBy", "Tutor");
//map.put("searchBy", "CourseName");
//map.put("startDate", new Date()); 
DynamicSqlMapper mapper = 
sqlSession.getMapper(DynamicSqlMapper.class); 
List courses = 
mapper.searchCourses(map);
for (Course course : courses){ 
System.out.println(course); 
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();




3.6.3 Where 条件
有时候,所有的查询条件应该是可选的。在需要使用至少一种查询条件的情况下,我们应该使用WHERE子句。并且如果有多个条件,我们需要在条件中添加AND或OR。
MyBatis提供了元素支持这种类型的动态SQL语句。 
在我们查询课程界面,我们假设所有的查询条件是可选的。然而,当需要提供一个或多个查询条件时,应该改使用WHERE子句。
 


元素只有在其内部标签有返回内容时才会在动态语句上插入WHERE条件语句。并且,如果WHERE子句以AND或者OR打头,则打头的AND或OR将会被移除。例如:如果参数tutorId的值为null,并且courseName参数值不为null,则标签会将AND name like #{courseName}中的AND移除掉,生成的SQL WHERE子句为:
where name like #{courseName}




3.6.4 条件
元素和元素类似,但是提供了在添加前缀/后缀或者移除前缀/后缀方面提供更大的灵活性。


prefix表示有一个if成立则插入where语句
suffix表示后缀,和prefix相反


suffixOverrides="and"表示如果最后生成的sql语句多一个and,则自动去掉.
prefixOverrides的意思是处理前缀,和suffixOverrides相反


这里如果任意一个条件为true,元素会插入WHERE,并且移除紧跟WHERE后面的AND 


3.6.5 foreach 循环
另外一个强大的动态SQL语句构造标签即是。它可以迭代遍历一个数组或者列表,构造AND/OR条件或一个IN子句。
假设我们想找到tutor_id为 1,3,6的讲师所教授的课程,我们可以传递一个tutor_id组成的列表给映射语句,然后通过遍历此列表构造动态SQL。
 

代码:
public interface DynamicSqlMapper{ 
List searchCoursesByTutors(Map map); 

public void searchCoursesByTutors(){ 
Map map = new HashMap(); 
List tutorIds = new ArrayList(); 
tutorIds.add(1); 
tutorIds.add(3); 
tutorIds.add(6); 
map.put("tutorIds", tutorIds); 
DynamicSqlMapper mapper = 
sqlSession.getMapper(DynamicSqlMapper.class); 
List courses = mapper.searchCoursesByTutors(map); 
for (Course course : courses){ 
System.out.println(course); 




怎样使用生成IN子句:
 






3.6.6 set 条件
元素和元素类似,如果其内部条件判断有任何内容返回时,他会插入SET SQL 片段。
 
 
update courses  
 
name=#{name}, 
description=#{description}, 
start_date=#{startDate}, 
end_date=#{endDate}, 
 
where course_id=#{courseId} 
 


这里,如果条件返回了任何文本内容,将会插入set关键字和其文本内容,并且会剔除将末尾的“,”。在上述的例子中,如果phone!=null,将会让会移除phone=#{phone}后的逗号“,”,生成set phone=#{phone} 






四 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 



 
 

select my_seq.nextval from dual

insert into t_user(id,name,gender) 
values(#{id},#{name},#{gender}) 



当你执行insertStudent语句的时候MyBatis会取Gender枚举(FEMALE/MALE)的名称,然后将其存储到GENDER列中。如果你希望存储原enum的顺序位置(0/1),而不是enum名,你需要明确地配置它
如果你想存储FEMALE为0,MALE为1到gender列中,你需要在mybatis-config.xml文件中配置EnumOrdinalTypeHandler: 
 


注意:使用顺序位置为值存储到数据库时要当心。顺序值是根据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或其他格式的。简介信息可以是学生或者讲师的漫长的人生经历。默认情况下,MyBatis将CLOB类型的列映射到java.lang.String类型上、而把BLOB列映射到byte[]类型上。
 
public class UserPic{ 
private int id; 
private String name; 
private byte[] pic; 
private String bio; 
//setters & getters 

 
 

select my_seq.nextval from dual

insert into user_pics(id,name, pic,bio) 
values(#{id},#{name},#{pic},#{bio}) 
 
 


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 findAllStudentsByNameEmail(String name, String email); 

MyBatis支持将多个输入参数传递给映射语句,并以#{param}的语法形式引用它们:
 
这里#{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中的话,需要使用下面的方式:

 


Map 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表示要取的记录的数目

 

public List findAllUsers(RowBounds rowBounds);


然后,你可以加载第一页数据(前5条):
int offset = 0;
int limit = 5; 
RowBounds rowBounds = new RowBounds(offset, limit); 
List = 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 map = new HashMap(); 
SqlSession session = null;
try {
session = MyBatisSqlSessionFactory.openSession();

session.select("com.briup.mappers.SpecialMapper.findAllUsers", new ResultHandler() {


@Override
public void handleResult(ResultContext 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:





xml映射文件:








测试代码:
@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标签常用属性:
eviction="FIFO"  
flushInterval="60000"
size="512"
readOnly="true"/>




五 MyBatis中的注解
之前我们都是在映射器MapperXML配置文件中配置映射语句的。除此之外MyBatis也支持使用注解来配置映射语句。当我们使用基于注解的映射器接口时,我们不再需要在XML配置文件中配置了。如果你愿意,你也可以同时使用基于XML和基于注解的映射语句。


5.1 在映射器Mapper接口上使用注解
MyBatis对于大部分的基于XML的映射器元素(包括