Mybatis配置及使用

来源:互联网 发布:部分图片来源于网络 编辑:程序博客网 时间:2024/06/08 10:23

MyBatis教程


 

目录

第1章 MyBatis起步...3

1-1 MyBatis概览...3

1-2下载MySQL.4

1-3 创建MyBatis项目...5

1-4 配置MyBatis.5

1-4-1 配置日志...6

1-4-2 配置类型别名...7

1-4-3 配置数据库连接和事务管理器...8

1-4-4 导入映射文件...8

1-5 编写工具类...10

1-6 映射和执行增删改查操作...11

第二章 增删改查操作...14

2-1 INSERT语句...14

2-1-1 参数类型...14

2-2-2 处理代理主键...17

2-2 UPDATE语句...19

2-2-1 简单的UPDATE.19

2-2-2 动态UPDATE.20

2-3 DELETE语句...22

2-4 SELECT语句...22

2-4-1 简单的SELECT语句...22

2-4-2 通过resultType属性指定结果集类型...23

2-4-3 在语句中插入动态内容...24

2-4-3 通过ResultMpper映射结果集...25

2-4-4 关联查询...25

2-4-5 动态SQL.28

2-4-6 分页查询...31

2-4-7 调用存储过程...36

第三章 性能优化...41

3-1 缓存...41

3-1-1 一级缓存...41

3-1-2 二级缓存...43

3-2 延迟加载...45

3-2-1 侵略性延迟加载...47

3-3 连接池...48

 


 

第1章 MyBatis起步

1-1 MyBatis概览

        

        

         上图为我们展示了MyBatis的组成部分和工作方式。

        

         首先是加载配置(Configuration)和映射(Mapper)。配置包括数据库连接方式、类型定义、缓存等选项。映射主要包含了各种需要通过MyBatis执行的语句(Statement)。映射可以通过XML和注解配置。这些语句在执行时可以接收Map集合、Java Bean、基本数据类型以及常用的引用类型,如String、Date等作为参数,同时也可以将执行结果封装成这些类型返回。

        

         同时,我们可以看到MyBatis不仅支持Java语言,同时也提供了.NET的实现版本。上图中SqlMapConfig.xml和SqlMap.xml指的是MyBatis的上一个版本,即iBatis的配置文件和映射文件。

 

         MyBatis的目的是解决传统JDBC操作中频繁发生的参数绑定与结果集封装操作造成的代码冗余问题。MyBatis并不是一个真正意义上的额ORM(对象关系映射)框架,并不映射实体类与表之间的对应关系,而是简单地映射了SQL语句接收哪些参数、返回什么样的结果之类的信息。

        

         使用MyBatis,所有SQL语句都需要手工编写,虽然这样会牺牲程序的可移植性,但同时也也提升了灵活性——我们可以在编写SQL语句时充分利用数据库的特性,或者编写复杂的查询解决实际问题。

 

         MyBatis主要具有以下特点:

l  自动绑定参数

         只需要在SQL语句中使用#{参数名}的方式设置占位符,MyBatis执行语句时可以动态地将参数绑定到这些占位符。参数绑定在底层通过PreparedStatement实现。

l  自动封装结果集

         只需要在映射SQL语句时指定结果集类型,或者手工设置结果集映射,MyBatis就可以将ResultSet中的数据封装为指定的类型并返回。结果集自动封装通过ResultSetMetaData实现。

l  动态SQL

         有时候,SQL语句的结构需要根据用户实际提供的条件动态创建,如多条件组合查询。可以在映射SQL语句时,通过一些特定的标签来设置动态SQL的生成规则。

l  查询接口

         可以定义一个Java接口,在接口中将SQL中映射的语句声明成对应的抽象方法。MyBatis可以在运行时动态生成这些接口的实现类对象。通过接口调用语句更符合面向对象的编程习惯,并且这些接口可以视为快速实现的DAO。

1-2下载MySQL

         MyBatis的官方网站为:http://code.google.com/p/mybatis/

         下载地址为: https://github.com/mybatis/mybatis-3/releases

        

         目前MyBatis的最新版本为3.2.7,本教程将基于3.2.2版本演示。

        

         MyBatis下载后的目录结构如下图所示:

        

         其中:

         mybatis-3.2.2.jar是MyBatis的核心jar文件,包含了MyBatis框架定义的各种接口和类。

         lib目录中存放的是MyBatis框架依赖的其他类库的jar文件,它们也需要引入项目。

         mybatis-apidocs目录中是MyBatis的API文档。

         mybatis-3.2.2.pdf是一个使用手册,里面系统地介绍了MyBatis的配置和使用方式。

1-3 创建MyBatis项目

         MyBatis属于数据持久层项目,因此可以在各个需要访问数据库的项目中使用MyBatis。

         我们的第一个项目是一个简单的Java应用程序,在创建项目后,需要在项目中引入下列jar文件:

        

        

         可以在项目中创建一个lib目录,将这些jar文件复制到lib目录中,再通过properties-JavaBuild Path-Labraries-Add Jars 将这些jar文件导入到项目。这样在移动项目时,这些jar文件及其引用关系一起移动。

 

         我们的第一个项目将使用ORACLE数据库,因此我们还导入了ORACLE的jdbc驱动ojdbc6.jar。

 

         为了掩饰MyBatis的使用,我们先在ORACLE下创建一个表,名称为Users,表结构如下:

        

列名

数据类型

说明

Id

NUMBER(10)

用户编号,主键

UserName

VARCHAR2(20)

用户名

Password

VARCHAR2(20)

密码

 

         接下来,规划一下项目的包结构:

        

entity

实体类

mapper

映射

test

测试类

 

         在entity包下创建一个实体类User,我们的准备工作就完成了。

1-4 配置MyBatis

         MyBatis的配置文件可以自由命名,在这里我们将其命名为mybatis-config.xml,创建在src目录,即class-path的根目录下。配置文件的主要结构如下:

 

 

<?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>

         <settings>

                  设置MyBatis各方面的特性

         </settings>

 

         <typeAliases>

                  设置类型别名

         </typeAliases>

        

         <environments>

                  设置数据库连接与事务特性

         </environments>

 

         <mappers>

        引入映射文件

         </mappers>

</configuration>

        

         MyBatis的配置文件的根元素是<configuration>,内部由以下几部分组成:

        

         <settings>:设置MyBatis各方面的特性,如日志、缓存等。

         <typeAliases>:设置MyBatis映射中使用的类型的别名。在MyBatis的映射中使用实体类时,需要指定实体类的完整类名。在<typeAliases>中为实体类设置别名后,映射文件中就可以通过别名来代替完整类名,这样可以简化映射文件的编写。

         <environments>:所谓的“环境”,是指与数据库连接与事务相关的配置。可以在<enviroments>中添加多个<enviroment>元素,分别设置程序在开发、测试、和正式部署时使用的数据库连接参数,并且将其中的一个设置为默认的环境。

         <mappers>:可以在项目中创建多个SQL映射文件,这些映射文件需要在<mappers>中通过<mapper>子元素引入,这样在MyBatis在读取配置文件时,才能加载这些SQL映射文件中的语句。

        

         下面,我们一步步完成配置文件的编写。

 

1-4-1 配置日志

       MyBatis在运行时可以通过日志工具输出一些有用的信息。使用什么日志工具可以在<settings>中设置:

        

 

<settings>

                  <setting name="logImpl" value="Log4J"/>          

</settings>

 

         <setting>元素的name属性指定了参数名,value属性指定了参数值。logImpl参数设置的是使用哪个日志实现,Log4J表示使用Log4J日志作为日志的实现。除了Log4J,还有以下选项:

        

SLF4J

通过SLF4J工具输出日志

LOG4J

通过Log4j工具输出日志

JDK_LOGGING

通过JDK的Logger输出日志

COMMONS_LOGGING

通过Apache的commons-login组件输出日志

STDOUT_LOGGING

向控制台输出

NO_LOGGING

不输出日志

        

         在这里,我们选择使用Log4J作为我们的日志工具。为此,我们需要在src下创建log4j.propoerties,内容如下:

 

log4j.rootLogger=debug, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.Target=System.out

log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

 

1-4-2 配置类型别名

         MyBatis内预定义了常用的Java类型的别名。我们在编写SQL映射时,可以使用这些别名来代替实际的Java类型名称。下面摘录其中的一部分:

                 

别名

Java类型

_int

int

_long

long

_double

double

_boolean

boolean

int

java.lang.Integer

long

java.lang.Long

double

java.lang.Double

boolean

java.lang.Boolean

string

java.lang.String

map

java.util.Map

list

java.util.List

         (更多信息,可以参考mybatis-3.2.2.pdf第12页内容)

         除了这些内置的别名,我们还可以为自己创建的类设置别名:

        

<typeAliases>

                  <typeAlias type="entity.User" alias="User"/>

</typeAliases>

 

         <typeAlias>元素的type属性设置了对象的完全限定名称,alias属性设置了别名。这样在SQL映射文件中,我们就可以用“User”来表示entity.User类型。

1-4-3 配置数据库连接和事务管理器

         数据连接和事务管理器在<environments>元素中通过<environment>元素配置。每个<environment>元素可以配置和一个特定数据的连接。尽管可以配置多个数据库连接,但是每个SqlSessionFactory在构建时只能选择使用其中的一个。关于SqlSessionFactory,在后面会有说明。

 

<environments default="dev">

         <environment id="dev">

                  <transactionManager type="JDBC"/>

                  <dataSource type="UNPOOLED">                                  

                           <property name="driver" value="oracle.jdbc.driver.OracleDriver"/>

                           <property name="url" value="jdbc:oracle:thin:@localhost:1521:ORCL"/>

                           <property name="username" value="system"/>

                           <property name="password" value="ok"/>

                  </dataSource>

         </environment>

</ environments>

 

         <environment>元素的id属性设置了“环境”的名称。<environments>元素的default属性指定了默认使用的环境的id。在这里我们默认使用的是叫做dev的环境。

 

         <transactionManager>元素的type属性设置了使用什么方式管理事务。JDBC表示通过JDBC的Connection接口的commit()和rollback()方法提交和回滚事务。

 

         <dataSource>元素设置了数据库连接的参数。type属性设置了数据源的类型。UNPOLLED表示不采用连接池,这样配置比较简单,只需要通过<property>元素设置驱动类名、URL、用户名和密码就可以了。

 

1-4-4 导入映射文件

         首先在mapper包下创建User-mapper.xml文件。该文件将作为操作User数据的映射文件。接下来在mybatis-config.xml中导入映射文件:

<mappers>

                  <mapper resource="mapper/User-mapper.xml"/>

</mappers>

 

         <mapper>元素的resource属性指定了映射文件的路径。

 

         至此,我们第一个MyBatis项目的配置文件就编写完成了。完整内容如下:

        

<?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>

         <settings>

                  <setting name="logImpl" value="Log4J"/>          

         </settings>

 

         <typeAliases>

                  <typeAlias type="entity.User" alias="User"/>

         </typeAliases>       

 

         <environments default="dev">

                  <environment id="dev">

                           <transactionManager type="JDBC"/>

                           <dataSource type="UNPOOLED">                                  

                                    <property name="driver" value="oracle.jdbc.driver.OracleDriver"/>

                                    <property name="url"

                                                      value="jdbc:oracle:thin:@localhost:1521:ORCL"/>

                                    <property name="username" value="system"/>

                                    <property name="password" value="ok"/>

                           </dataSource>

                  </environment>

         </ environments>

 

         <mappers>

                  <mapper resource="mapper/User-mapper.xml"/>

         </mappers>

</configuration>

 


 

1-5 编写工具类

         MyBatis通过SqlSession对象执行各种被映射的语句,SqlSession需要通过SqlSessonFactory创建。而SqlSessonFactory则需要使用SqlSessionFactoryBuilder基于配置构建。

        

Reader reader=Resources.getResourceAsReader("mybatis-config.xml");

SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();

sessionFactory=builder.build(reader);

 

         尽管可以使用任意的InputStram构建SqlSessionFactory,但推荐做法是将MyBatis的配置文件放在类路径下。MyBatis提供了一个叫做Resources的工具类,这个类中包含了一些用来简化从类路径加载资源的方法。

        

         拿到SqlSessionFactory后,我们就可以通过它来获取SqlSession了。SqlSessionFactory提供了一系列重载的openSession()方法,在这里先列出两个:

 

l  SqlSession openSession()

通过无参的openSession()方法创建的SqlSession对象,该对象不会自动提交事务。

l  SqlSession openSession(boolean autoCommit)

                  如果想手工指定事务是否自动提交,在调用openSession()方法时传递一个布尔值。如果传递true,则表示启用自动提交事务,否则表示不自动提交事务,而是通过手工方式结束事务。

        

         为了方便在程序中使用SqlSession,我们可以编写一个工具类MybatisUtil:

 

public class MyBatisUtil {

         private static final Logger logger=Logger.getLogger(MyBatisUtil.class);

         private static SqlSessionFactory sessionFactory;

 

         static{

                  try{

                          Reader reader=Resources.getResourceAsReader("mybatis-config.xml");

                           SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();

                           sessionFactory=builder.build(reader);

                  }catch(Exception ex){

                          logger.error("创建SqlSessionFactory时发生错误",ex);

                  }

         }

         public static SqlSession openSession(){

                  return sessionFactory.openSession();

         }

}

1-6 映射和执行增删改查操作

         在我们来编辑mapper包下的User-mapper.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="mapper.UserMapper">

 

</mapper>

        

         <mapper>是SQL映射文件的根元素。namespace属性设置了命名空间。命名空间最主要的作用是解决SQL语句的命名冲突问题。在映射文件中,每个SQL语句都有一个名字,我们通过这些名字来指定要调用哪个语句。但是多个映射文件中可能存在同名的语句,因此可以通过指定不同的命名空间来解决SQL语句同名的问题。当然,在同一个命名空间中的SQL语句是不能重名的。

        

         下面,我们就来映射第一个语句。这是一个用来向Users表插入数据的Insert语句:

 

<insert  id="insertUser" parameterType="User">

<![CDATA[

         INSERT INTO Users(id,userName,password)

         VALUES(#{id},#{userName},#{password})

]]>

</insert>

 

         INISERT语句通过<insert>元素映射。id属性指定了该语句的名字,在同一个命名空间中id不能重复。parameter属性指定了执行这条INSERT语句时使用的参数类型。User是我们在mybatis-config.xml中配置的一个类型别名,表示entity.User类型。也就是说,我们在执行这条语句时,需要传递一个User对象作为参数,要插入的数据封装在这个对象中。

        

         <insert>元素的内容是一个INSERT语句,包含在一个CDATA块中。CDATA块是XML中的一个语法结构,包含在CDATA块中的内容会作为普通文本解析,如果语句中包含诸如>、<等特殊符号,就应当包含在CDATA块中。

 

         接下来,我们会注意到在INSERT语句的VALUES子句中,使用了#{}的方式设置了一些参数的占位符。#{}里面写的是参数对象的属性名。在这里,我们的参数是User类型的,那么#{id}就代表将User对象的id属性值绑定到这个占位符,而#{userName}和#{password}则分别表示绑定User对象的userName属性和password属性。

         在执行时,这个语句会首先被转换为

         INSERTINTO Users(id,userName,password) VALUES(?,?,?)

         然后,再根据占位符的描述,分别取出参数user对象的id、userName和password属性的值,依次绑定到三个占位符(?)上。

        

         下面我们来执行这条语句:

        

SqlSession session=null;

try {

         User user=new User();

         user.setUserName("tom");

         user.setPassword("123456");

         session=MyBatisUtil.openSession();

         session.insert("mapper.UserMapper.insertUser",user);

         session.commit();                   

} catch (Exception ex) {

         ex.printStackTrace();

         session.rollback();

}

finally{

         session.close();

}

 

         SqlSession的insert方法用来执行映射的INSERT语句。该方法的第一个参数是要执行的语句的完整名称。所谓完整名称是指命名空间+语句ID。在之前编写SQL映射文件时,我们将命名空间设置为mapper.UserMapper,而<insert>的ID设置为insertUser,因此这条语句的完整名称为mapper.UserMapper.insertUser。第二个参数是我们要传递给这个语句的参数。

 

         同时我们还必须注意到事务的控制。通过SqlSessionFactory的无参openSession()方法获取的SqlSession将关闭事务的自动提交,因此我们必须在执行操作后,调用SqlSession对象的commit()方法提交事务,并且在catch块中调用rollback()方法回滚事务。

 

         还有另一种更方便的执行SQL语句的方式,就是通过映射接口。下面,我们在mapper包下创建一个接口,名字叫做UserMapper:

 

public interface UserMapper{

         public int insertUser(User user);

}

 

         这个接口的完全限定名是mapper.UserMapper,没错,正好就是我们SQL映射文件使用的命名空间。也就是说,如果要想通过接口来执行语句,那么就需要将映射文件的命名空间设置为接口的完全限定名称。此外,接口中的方法名,也正是映射文件中语句的ID。方法的参数与SQL语句声明的参数类型也一致。如果执行的是INSERT语句,则返回值为int类型,表示受影响的行数。

 

         下面我们就来看一下如何通过这个接口来执行插入用户的操作:

 

SqlSession session=null;

try {

         User user=new User();

         user.setUserName("tom");

         user.setPassword("123456");

         session=MyBatisUtil.openSession();

    UserMapper userMapper=session.getMapper(UserMapper.class);

         userMapper.insertUser(user);

         session.commit();                   

} catch (Exception ex) {

         ex.printStackTrace();

         session.rollback();

}

finally{

         session.close();

}

 

         通过SqlSession对象的getMapper()方法,获得一个UserMapper类型的对象。这个对象时MyBatis在运行时动态生成的。接下来就可以调用在接口中声明的方法来执行插入用户的操作了。这种方式更符合面向对象的编程习惯,也是我们后面主要使用的调用方式。

 

         好了,到目前为止,我们的MyBatis项目已经开始工作了。下面我们将深入讲解MyBatis各种语句的映射和调用。


 

第二章增删改查操作

2-1 INSERT语句

         我们已经知道,INSERT语句通过<insert>元素映射。下面我们来深入地了解一下与INSERT语句语句的映射与执行相关的问题。

 

2-1-1 参数类型

调用INSERT语句时可以传递以下类型的参数:

(1)JavaBean

         使用JavaBean传参,需要将<insert>元素的parameterType属性设置为JavaBean的完全限定名称,或者使用在配置文件中设置的别名。如:

 

<insert  id="insertUser"  parameterType="User">

         INSERT INTO User(id,userName,password) VALUES(#{id},#{userName},#{password})

</insert>

 

         在Mapper接口中的抽象方法声明为:

 

public int insertUser(User user);

 

         调用时,需要将待插入的数据先添加到一个User对象中。如:

 

......

UserMapper userMapper=sqlSession.getMapper(UserMpaper.class);

User user=new User();

user.setUserName("admin");

user.setPassword("123456");

int rows=userMapper.insertUser(user);

......

 

 

(2)Map

         使用Map传参,需要将<insert>元素的parameterType属性设置为map,如:

        

<insert  id="insertUser"  parameterType="map">

         INSERT INTO User(id,userName,password) VALUES(#{id},#{userName},#{password})

</insert>

 

         在Mapper接口中声明的抽象方法是:

 

public int insertUser(Map user);

 

         调用时,需要将待插入的数据先添加到一个Map集合中,如:

 

......

UserMapper userMapper=sqlSession.getMapper(UserMpaper.class);

Map<String,Object> map=new HashMap<String,Object>();

map.put("userName","admin");

map.put("password","123456");

int rows=userMapper.insertUser(map);

......

 

(3)对象数组

         可以使用对象数组为INSERT语句传参。这时不需要设置<insert>元素的parameterType属性,在INSERT语句内部通过“array”关键字引用参数数组,如:

 

<insert  id="insertUser">

         INSERT INTO User(id,userName,password) VALUES(#{array[0]},#{array[1]},#{array[2]})

</insert>

 

         在上面的例子中,INSERT语句中通过#{array[0]}、#{array[0]}、#{array[2]}分别引用了参数数组中第1个、第2个和第3个元素。

 

         在Mapper接口中声明的抽象方法是:

 

public int insertUser(Object[] params);

 

         调用时,需要将待插入的数据存入一个对象数组中,如:

        

......

UserMapper userMapper=sqlSession.getMapper(UserMpaper.class);

Object[] params={"admin","123456"};

int rows=userMapper.insertUser(map);

......

 

         如果不想在INSERT语句中使用array关键字引用参数数组,可以在定义Mapper接口的方法时使用@Param注解指定参数的名称,如:

        

public int insertUser(@Param("params") Object[] params);

 

         这样,在INSERT语句中,就可以使用“params”来引用参数数组了,如:

 

<insert  id="insertUser">

         INSERT INTO User(id,userName,password)

         VALUES(#{params[0]},#{params[1]},#{params[2]})

</insert>

 

(4)独立参数

         在定义Mapper接口的方法时,还可以将待插入的数据分别定义成独立的参数,如:

        

public int insertUser(int id,String userName,String password);

 

         这样,不需要设置<insert>元素的parameterType属性,在INSERT语句中可以通过#{0}、#{1}、#{2}分别引用第1个、第2个和第3个参数,如:

 

<insert  id="insertUser">

         INSERT INTO User(id,userName,password) VALUES(#{0},#{1},#{2})

</insert>

        

         在调用时,只需要按顺序传参即可。如:

        

......

UserMapper userMapper=sqlSession.getMapper(UserMpaper.class);

int rows=userMapper.insertUser(1,"admin","123456");

......

        

         如果在INSERT语句中不想通过#{0}、#{1}、#{2}这样的方式引用参数,则需要在Mapper接口的方法中使用@Param注解再来指定参数的名称,如:

        

public int insertUser(@Param("id") int id,

                                     @Param("userName") String userName,

                                     @Param("password") String password);

 

         这样,INSERT语句就可以写成:

        

<insert  id="insertUser">

         INSERT INTO User(id,userName,password) VALUES(#{id},#{userName},#{password})

</insert>

 


 

2-2-2 处理代理主键

         根据主键的生成方式,数据库的主键主要可以分为两种:自然主键和代理主键。

 

         所谓自然主键,是指使用业务数据中的字段作为主键。这些字段“看上去”也具有唯一性,如身份证号。使用自然主键可能会有一些问题,因为根据数据库的设计原则,主键应当是唯一的、对用户无意义的且不会被修改的。而自然主键由于本身也是业务数据,因此对于用户来说是有意义的,那么就有可能会出现被修改的状况。如果是新的项目,在设计数据库时应当避免使用自然主键,而采用代理主键。

 

         所谓代理主键,是指由数据库自动生成生成的主键。代理主键满足唯一、无意义特性,并且由于其是无意义的,因此不会被用户修改。目前市面上流行的数据库分别提供了不同的代理主键实现手段。

 

         那么,在MyBatis中执行INSERT语句时,如何使用数据库的代理主键机制,以及如何在INSERT语句执行后,获取生成的代理主键呢?首先,来看一下ORACLE数据库。ORACLE数据库采用序列生成代理主键。首先需要创建序列:

 

CREATE SEQUENCE Users_SEQ;

 

         然后再执行INSERT语句时,可以通过Users.nextval获取序列生成的下一个主键值:

        

INSERT INTO Users(id,userName,password) VALUES(Users_SEQ.nextval,'admin','123456');

 

         下面就来看一下在MyBatis中如何映射使用序列的INSERT语句。如果我们只是想在插入数据时使用序列生成主键,而不需要MyBatis返回主键值,则可以简单地将INSERT语句映射为:

 

<insert  id="insertUser" parameterType="User">

         INSERT INTO Users(id,userName,password)

         VALUES(Users_SEQ.nextval,#{userName},#{password})

</insert>

 

         但是如果我们如果想要在执行操作后让生成的主键值自动填充到参数User对象的id属性中,就要进行如下映射:

 

<insert  id="insertUser" parameterType="User">

         <selectKey  order="BEFORE" resultType="int" keyProperty="id">

                  SELECT Users_SEQ.nextval FROM DUAL

         </selectKey>

         INSERT INTO Users(id,userName,password)

         VALUES(#{id},#{userName},#{password})

</insert>

         在<insert>元素中,使用了<selectKey>元素设置了查询序列的SELECT语句。order属性设置了这条SELECT语句的执行次序,BEFORE表示在INSERT之前先执行SELECT语句查询序列的下一个值;resultType属性设置了将查询的结果以int类型返回;keyProperty属性设置了将SELECT语句返回的序列值填充到参数User对象的id属性中。

 

         这样,在执行INSERT语句时,参数User对象的id属性已经被填充了通过INSERT语句获取的序列值,在INSERT语句中,可以直接使用#{id}绑定参数User对象的id属性。

 

         而对于SQL Server和MySQL数据库,它们支持所谓的“自增主键”。也就是在执行INSERT语句时,主键列的值可以自动生成。对于SQL Server,可以将主键设置为标识列(identity),而MySQL,则可以为主键设置自动增长选项(AUTO_INCREMENT)。如果使用<selectKey>获取自动生成的主键值,SQL Server可以写成:

 

<insert  id="insertUser" parameterType="User">

         <selectKey  order="AFTER" resultType="int" keyProperty="id">

                  SELECT @@identity

         </selectKey>

         INSERT INTO Users(userName,password)  VALUES(#{userName},#{password})

</insert>

        

         由于标识列的值不是在插入前生成的,因此需要将<selectKey>的order属性设置为AFTER,即在执行完INSERT后通过查询@@identity全局变量获取生成的标识值,而INSERT语句中不包含id列。这样INSERT操作完成后,参数User对象的id属性同样会被填充主键值。而对于MySQL的自增主键,则可以通过查询LAST_INSERT_ID()函数获取上次INSERT语句生成的主键值:

 

<insert  id="insertUser" parameterType="User">

         <selectKey  order="AFTER" resultType="int" keyProperty="id">

                  SELECT LAST_INSERT_ID()

         </selectKey>

         INSERT INTO Users(userName,password)  VALUES(#{userName},#{password})

</insert>

 

         对于自动生成的主键值,除了使用<selectKey>获取,还有一种更加简单的方式,那就是使用<insert>元素的useGeneratedKey属性:

        

<insert

         id="insertUser"

         parameterType="User"

         useGeneratedKeys="true"

         keyProperty="id">

         INSERT INTO Users(userName,password)  VALUES(#{userName},#{password})

</insert>

 

         将useGeneratedKeys设为true,表示通过java.sql.Statement中定义的getGeneratedKeys()方法获得语句自动生成的键值,并且填充到参数对象中由keyProperty指定的属性中。前提数据库驱动支持这个方法,否则将抛出SQLFeatureNotSupportedException异常。

 

         实际上,如果使用Map集合作为参数对象,生成的键值依然可以被添加到集合中,这是keyProperty属性指定的将是Map的KEY。

 

2-2 UPDATE语句

2-2-1 简单的UPDATE

         简单的UPDATE通过<update>元素映射,其接收的参数类型与<insert>元素相同。例如,如果将User对象为参数,可以写成:

 

<update  id="updateUser" parameterType="User">

         UPDATE User SET userName=#{userName},password=#{password} WHERE id=#{id}

</update>

 

         对应的Mapper接口方法为:

 

public int updateUser(User user);

 

         如果将Map集合作为参数,可以写成:

 

<update  id="updateUser" parameterType="Map">

         UPDATE User SET userName=#{userName},password=#{password} WHERE id=#{id}

</update>

 

         对应的Mapper接口方法为:

 

public int updateUser(Map params);

 

         如果将对象数组作为参数,可以写成:

 

<update  id="updateUser">

         UPDATE User SET userName=#{array[1]},password=#{array[2]}

         WHERE id=#{array[0]}

</update>

 

         对应的Mapper接口方法为:

 

public int updateUser(Object[] params);

 

         如果采取独立传参的方式,例如将接口方法定义为:

 

public int updateUser(int id,String userName,String password);

 

         则对应的<update>元素可以写成:

 

<update  id="updateUser">

         UPDATE User SET userName={1},password{2} WHERE id={0}

</update>

 

         如果想为各个参数指定参数名,则可以将接口方法定义为:

 

public int updateUser(

         @Param("id") int id,

         @Param("userName") String userName,

         @Param("password") String password

);

 

         对应的<update>元素可以写成:

 

<update  id="updateUser">

         UPDATE userName=#{userName},password=#{password} WHERE id=#{id}

</update>

 

         至于UPDATE语句的调用方式,与INSERT语句类似,在这里就不再重复罗列代码。

2-2-2 动态UPDATE

         有时在执行UPDATE语句时,参数对象可能有一部分属性值为NULL,而执行的UPDATE语句应当只使用那些不为NULL的属性,这时就需要在运行时通过判断决定将哪些属性包含到UPDATE语句中。MyBatis提供了一系列用来动态生成SQL的标签,其中大部分用于动态生成SELECT语句,但针对UPDATE语句的特别提供了一个<set>元素。下面来看一个例子:

 

<update  id="updateUser" parameterType="User">

         UPDATE

         <set>

                  <if test="userName!=null">userName=#{userName},</if>

                  <if test="password!=null">password=#{password}</if>

         </set>

         WHERE id=#{id}

</update>

         <if>元素通过test属性设置条件表达式,如果表达式结果为true,则会将<if>元素的内容添加到SQL语句中。如果参数User对象的userName和password属性都不为NULL,则两个<if>元素生成的内容将是:

        

userName=#{userName},

password=#{password}

 

         然后,外围的<set>元素会生成最终的SET子句:

 

SET

         userName=#{userName},

         password=#{password}

 

         最终执行的完整的UPDATE语句将是:

 

UPDATE User

SET

         userName=#{userName},

         password=#{password}

WHERE id=#{id}

 

         但是,如果参数User对象的password属性为NULL,则着两个<if>元素生成的内容将是:

        

userName=#{userName},

 

         外围的<set>元素在生成SET子句时,会将最后多余的“,”去掉,结果是:

 

UPDATE User

SET

         userName=#{userName}

WHERE id=#{id}

 

         还有一个<trim>元素也可以实现相同的效果,只是语法看上去不如<set>简介:

 

<update  id="updateUser" parameterType="User">

         UPDATE

         <trim prefix="SET" suffixOverrides=",">

                  <if test="userName!=null">userName=#{userName},</if>

                  <if test="password!=null">password=#{password}</if>

         </set>

         WHERE id=#{id}

</update>

 

         prefix属性设置了在内容前面添加“SET”,suffixOverrides属性表示替换掉最后的“,”。

2-3 DELETE语句

         DELETE语句是最简单的一种DML语句,最常见的一种DELETE就是根据ID删除一条记录:

 

<delete  id="delUser">

         DELETE FROM User WHERE id=#{id}

</delete>

 

         对应的Mpper接口方法为:

 

public int delUser(int id);

 

         在这里,需要补充说名一个问题,即如果只传递一个参数,则这个参数在SQL语句中可以用任意的名称引用。上面的例子中,也可以将#{id}写成#{userId},#{0}。因为只有一个参数,所以可以明确的和唯一的占位符对应上,因此对于占位符的命名可以随意。但是如果我们想限定在语句中使用的占位符名称,也可以在Mapper接口方法的参数上使用@Param注解:

        

public int delUser(@Param("id") int id);

        

         这样,在SQL语句中,就只能使用#{id}做占位符,使用其他名称命名占位符将发生异常。这一点在映射其他语句,如SELECT语句时也适用。

 

         至此,我们已经了解了如何在MyBatis中映射和INSERT、UPDATE、DELETE语句。需要再次强调的是,执行这些语句时,我们必须显式地通过SqlSession对象的commit()和rollback()方法控制事务的提交和回滚。

 

2-4 SELECT语句

         在CURD操作中,使用最频繁,形式最灵活的便是查询操作了。因此,MyBatis对SELECT语句提供了最丰富的支持。

2-4-1 简单的SELECT语句

         最常见,也是最简单的一种查询就是返回表中全部的数据,并且将每一行数据封装成实体类对象:

 

<select id="getBrandBeanList" resultType="Brand">

         SELECT id,name FROM Brand ORDER BY id

</select>

         在这里,我们查询了品牌(Brand)表中的全部数据,并且通过resultType属性指定将每一行数据封装为一个别名为"Brand"的实体类对象。也就是说,我们需要在配置文件中设置"Brand"这个别名:

                 

<typeAliases>

         <typeAlias type="bdqn.camera.entity.Brand" alias="Brand"/>

</typeAliases>

 

         对应的Mapper接口方法为:

        

public List<Brand> getBrandBeanList();

 

         在上面的查询中,我们没有设置任何条件,但是在实际的应用中,查询往往是带有条件的,并且这些条件有时候会以非常复杂的逻辑进行组合。在后面的内容中我们将循序渐进地介绍如何在SELECT语句中使用条件。下面我们先来看一个同样常见的查询:根据ID获得单行数据:

 

<select id="getBrandBeanById" resultType="Brand">

         SELECT id,name FROM Brand WHERE id=#{id}

</select>

 

         在SELECT语句中,我们使用了#{id}作为参数占位符。对应的Mapper接口方法为:

 

public Brand getBrandBeanById(Integer id);

 

         因为我们使用主键作为条件,所以可以确保返回的数据最多只有一行。在这种情况下,我们可以将方法的返回值设置为单个实体对象。

 

2-4-2 通过resultType属性指定结果集类型

         在上面的例子中,我们都是将结果中的每行数据封装为实体类对象。其实,我们还可以通过resultType指定将查询结果中的每行数据以其他的形式封装。

 

         由于SELECT语句的灵活性,我们执行的多个查询可能会返回不同结构的结果集。如果我们坚持用JavaBean来进行封装,则有可能会定义定义很多这样JavaBean,且一旦SELECT语句进行了调整,对应的JavaBean也要进行修改。因此可以将结果集中的每行数据封装为Map对象:

 

<select id="getBrandMapList" resultType="hashmap">

         SELECT id,name FROM Brand ORDER BY id

</select>

 

         我们将resultType设置成了hashmap,这样MyBatis会将结果集中的每行数据封装为一个HashMap对象。对应的Mapper接口方法为:

 

public List<Map<String,Object>> getBrandMapList();

 

         在封装结果集时,MyBatis将根据ResultSetMetaData(结果集元数据)来获得列名和类型信息。由于从ResultSetMetaData中获取的列名是全大写形式的,因此我们在从Map中取出数据时,也要使用全大写形式的列名:

 

......

List<Map<String,Object>> brands=brandMapper.getBrandMapList();

for(Map<String,Object> brand:brands){

         System.out.println(brand.get("ID")+":"+brand.get("NAME"));

}

......

 

2-4-3 在语句中插入动态内容

         再来考虑下一个问题。有的时候用户除了可以设置查询条件,还可以设置排序规则。也就是说SELECT指令中的ORDER BY子句也是可以动态改变的。这时,我们可以将ORDER BY子句以参数的方式传入,并且动态地插入到SELECT语句中:

        

<select id="getBrandBeanList" resultType="Brand">

         SELECT id,name FROM Brand ORDER BY ${order_col}

</select>

 

         在这里,我们使用了${order_col}占位符。${}占位符可以直接将参数内容替换到查询语句中。而#{}展位符则会转化为语句中的 ? 占位符,并且通过PreparedStatement进行绑定。

 

         对应的Mapper接口方法为:

        

public List<Brand> getBrandBeanList(@Param("order_col")String order_col);

        

         在这里,我们必须通过@Param注解将方法参数对应到${order_col}占位符。

 

         如果像下面这样调用getBrandBeanList方法:

        

...

BrandMapper brandMapper=session.getMapper(BrandMapper.class);

List<Brand> brands=brandMapper.getBrandBeanList("id asc");

...

        

         则实际执行的SQL语句为:

                 

SELECT id,name FROM Brand ORDER BY id asc

 

       可以看到,SELECT语句中的${order_col}占位符直接替换成了调用方法时使用的的参数"id asc"。

2-4-3 通过ResultMpper映射结果集

         之前我们在演示使用hashmap封装结果类型时说到MyBatis会根据ResultSetMetaData来获取列名和类型信息。这样会导致在Map集合中的列名是全大写的,并且数据类型也有可能和我们的预期不同。那么如何才能既使用Map封装数据以减少实体类的数量,又能显式指定每一列在Map中对应的的键(key)以及值(value)的数据类型呢?答案是使用ResultMapper来映射结果集。

 

         下面我们就来编写一个将Brand表的查询结果封装成Map集合的ResultMapper:

 

<resultMap type="hashmap" id="brandMap">

         <id column="id" property="id" javaType="int"/>

         <result column="name" property="name" javaType="string"/>

</resultMap>

 

         我们通过<resultMap>元素来定义结果映射,id是结果映射的名字,用来被<select>元素引用,type指定了封装的类型,在这里我们表示使用Map集合封装。

 

         在<resultMap>元素内,通过<id>元素映射了标识列(主键)如何封装。之后通过<result>子元素映射了结果集中其他列的封装。column是列名,property是属性名或者键(ley),javaType指定了数据对应的java类型。这样,我们不仅指定了Map集合中的列名,而且还显式指定了各个列在封装时的java类型。

 

         使用<id>元素封装主键,可以带来一些性能方面的优化,特别是在使用缓存和嵌套结果映射时。

 

         接下来,在<select>元素中,使用resultMap指定要使用的结果映射:

 

<select id="getBrandMapList" resultMap="brandMap">

         SELECT id,name FROM Brand ORDER BY id

</select>

 

2-4-4 关联查询

         数据库中的表往往不是孤立地存在,而是与其他的表存在关联,这种关联关系通常由主外键描述,常见的关系有一对一、一对多、多对多等。这种表之间的关联关系在程序中体现为实体类的相互嵌套。这就意味着当我们使用连接查询获取来自多个关联表的数据时,也希望把这些数据封装为实体类的嵌套结构。这种转换需要通过结果映射(ResultMap)来描述。

 

         下面我们先来看一个一对多查询。品牌(Brand)和相机(Camera)之间存在一对多关联关系,相机(Camera)表通过外键brandId品牌(Brand)表主键。在实体类Brand中,有一个表示一对多关联的集合cameras:

        

private Integer id;

private String name;

private List<Camera> cameras;

 

         下面我们来映射一个左外连接查询:

        

<select id="getBrandWithCamera" resultMap="brandWithCamera">

                  SELECT

                          b.id as brandId,

                          b.name,

                          c.id as cameraId,

                          c.brandId as c_brandId,                         

                          c.title,

                          c.price

                  FROM Brand b LEFT OUTER JOIN Camera c ON b.id=c.brandId       

</select>

 

         注意,由于brand表和camera表存在id列,因此我们必须通过分配别名的方式来区分这些列。接下来我们看一下结果映射:

 

<resultMap type="Brand" id="brandWithCamera">

         <id column="brandId" property="id"/>

         <resultcolumn="name" property="name"/>

         <collection

            property="cameras"

            column="c_brandId"

            javaType="list"

            ofType="Camera">

                  < id column="cameraId" property="id"/>

                  < result column="title" property="title"/>

                  < result column="price" property="price"/>

         </collection> 

</resultMap>

 

         首先,<resultMap>的type属性设置为Brand,表示结果集中的每一行数据将被映射为Brand对象。<id>和<result>分别映射了brandId和name的封装。

         接下来,通过<collection>元素映射了Brand类中的cameras集合如何封装。property表示集合的属性名,column表示在结果集中用来划分集合的列,在这一列上具有相同值的数据将被添加到同一个集合中,javaType表示集合的类型为List类型,ofType表示集合中包含的是Camera实体类对象。

 

         然后我们再看一下内连接查询。相机(Camera)类中有一个brand属性,表示其与品牌(Brand)之间的多对一关联:

 

        

public class Camera {

         private Integer id;

         private Integer brandId;

         private Integer typeId;

         private String title;

         ......

         private Brand brand;

    ......

}

 

 

<select id="getCameraWithBrand" resultMap="cameraWithBrand">

         SELECT

                  c.id as cameraId,

                  c.title,

                  c.price,

                  b.id as brandId,

                  b.name                                      

         FROM Camera c INNER JOIN Brand b ON c.brandId=b.id

</select>

 

<resultMap type="Camera" id="cameraWithBrand">

         <id column="cameraId" property="id"/>

         <result column="title" property="title"/>

         <result column="price" property="price"/>

         <association property="brand" javaType="Brand">

                  <id column="brandId" property="id"/>

                  <result column="name" property="name"/>

         </association>

</resultMap>

 

         咋这里,我们使用了<association>元素来映射与Camera关联的Brand对象的封装。property表示在Camera中对应的属性名,javaType表示封装的Java类型。在<association>元素内部我们通<id>和<result>元素描述了将结果集中的哪些列封装到brand元素中。

2-4-5 动态SQL

         在很多情况下,查询条件并非固定不变的,而是需要根据用户的设置进行动态的组合。比如一个复杂的查询表单最多允许用户设置十个个查询条件,但用户不一定每一次设置全部的十个条件,有可能只设置其中的几个条件,或者一个也不设置。这就要求我们必须根据用户实际设置的条件来动态生成WHERE子句。下面我们就来看一下MyBatis如何实现这一点。

●通过<if>动态生成SQL语句

<select id="getCameraBeanList" resultType="Camera" parameterType="hashmap">

         SELECT id,title,price FROM Camera

         <if test="title!=null">

                  WHERE title LIKE #{title}                                         

         </if>

         ORDER BY id

</select>

 

         首先,我们注意到在这里parameterType变成了hashmap,也就是说在调用这个语句时可以传递一个Map集合作为参数。接下来,在SELECT语句中使用了<if>元素进行了条件判断。test指定的判断表达式。在这里表示如果作为参数的Map集合中存在与title对应的值,则会在生成的SELET语句中包含WHERE部分,否则不包含WHERE部分。下面我们来看一下对应的Mapper接口方法:

 

public List<Camera> getCameraBeanList(Map<String,Object> params);

 

         再来看调用的代码:

        

CameraMapper cameraMapper=session.getMapper(CameraMapper.class);

Map<String,Object> params=new HashMap<String, Object>();

params.put("title","%便携%");

List<Camera> cameras=cameraMapper.getCameraBeanList(params);

 

         在这里我们创建了一个Map集合params,并且添加了title参数。接下来我们使用该Map集合调用了getCameraBeanList方法。这样将会执行下列SELECT语句:

        

SELECT id,title,price FROM Camera WHERE title LIKE ? ORDER BY id

 

         反之,如果我们这样调用:

        

CameraMapper cameraMapper=session.getMapper(CameraMapper.class);

List<Camera> cameras=cameraMapper.getCameraBeanList(null);

 

         这样,实际执行的SELECT语句将是:

 

SELECT id,title,price FROM Camera ORDER BY id

 

         在上面的例子中,我们在添加查询条件时,就将"%"通配符添加在了查询条件的两端:

        

params.put("title","%便携%");

 

         如果我们不希望在Java代码中拼接通配符,可以在<select>元素中添加<bind>元素:

        

<select id="getCameraBeanList" resultType="Camera" parameterType="hashmap">

         <bind name="_title" value="'%' + _parameter.title + '%'" />

         SELECT id,title,price FROM Camera

         <if test="title!=null">

                          WHERE title LIKE #{_title}                                      

         </if>

         ORDER BY id

</select>

 

         <bind>元素的name属性指定了一个变量名,而value属性则是一个OGNL表达式,可以动态地计算出一个新的值。在表达式中"_parameter"表示调用语句时使用的参数。在这里我们将title前后拼接'%',并设置对应的变量名为"_title",接下来我们就可以通过#{_title}使用这个变量了。

 

         这样做,在调用时就不用在Java代码中添加通配符了:

                 

params.put("title","便携");

 

●通过<foreach>生成参数列表

         有的时候,用户可能会通过一组复选框来设置条件,这种针对特定字段且数量不定的条件在SELECT语句中通常对应“IN(.....) ”结构。在MyBatis中,可以通过在<select>中使用<foreach>来动态生成"IN(....)"条件列表:

        

<select id="getCameraBeanList" resultType="Camera" parameterType="hashmap">

         SELECT id,title,price FROM Camera WHERE id IN

         <foreach item="id" collection="ids" open="(" separator="," close=")">

                  #{id}

         </foreach>

         ORDER BY id

</select>

 

         <foreach>元素的collection属性设置了参数集合对应的属性名,item表示用来引用历时从参数集合中取出的每个元素的变量名,open表示在开始前输出的内容,close、表示在遍历结束后输出的内容,sperator表示在遍历时两个元素之间输出的分隔符。

 

         如果我们像这样调用:

        

Map<String,Object> params=new HashMap<String, Object>();

params.put("ids",new Integer[]{1,2,3});

List<Camera> cameras=cameraMapper.getCameraBeanList(params);

 

         则实际执行的SELECT语句将是:

        

SELECT id,title,price FROM Camera WHERE id IN ( ? , ? , ? ) ORDER BY id

        

●通过<where>动态组合条件

         往往我们要动态加入的不只是一个条件,而是多个条件动态组合。这时需既需要考虑where子句是否生成,还要考虑是否在每个条件前面是否添加"AND"或"OR"。这时我们可以通过<where>元素组合这些条件:

 

<select id="getCameraBeanList" resultType="Camera" parameterType="hashmap">

         SELECT id,title,price FROM Camera

         <where>

                  <if test="title!=null">title LIKE #{title}</if>

                  <if test="ids!=null">

                          AND id IN

                          <foreach item="id" collection="ids" open="(" separator="," close=")">

                          #{id}

                          </foreach>                                      

                  </if>

         </where>

         ORDER BY id

</select>

 

         我们看到在<where>中包含<if>元素判断是否添加相应的条件。如果我们像下面这样调用:

 

Map<String,Object> params=new HashMap<String, Object>();

params.put("title","便携");

params.put("ids",new Integer[]{1,2,3});

List<Camera> cameras=cameraMapper.getCameraBeanList(params);

 

         在这里我们向参数集合中添加了两个条件,则实际执行的SELECT语句为:

        

SELECT id,title,price FROM Camera WHERE title LIKE ? AND id IN ( ? , ? , ? ) ORDER BY id

 

         如果我们这样调用:

 

Map<String,Object> params=new HashMap<String, Object>();

params.put("title","便携");

List<Camera> cameras=cameraMapper.getCameraBeanList(params);

        

         在这里我们只添加了title条件,则实际执行的SELECT语句为:

 

SELECT id,title,price FROM Camera WHERE title LIKE ? ORDER BY id

 

         而如果我们这样调用:

 

Map<String,Object> params=new HashMap<String, Object>();

params.put("ids",new Integer[]{1,2,3});

List<Camera> cameras=cameraMapper.getCameraBeanList(params);

 

         在这里我们只添加了ids数组,则实际执行的SELECT语句为:

        

SELECT id,title,price FROM Camera WHERE id IN ( ? , ? , ? ) ORDER BY id

        

         而如果我们不添加任何条件调用:

        

List<Camera> cameras=cameraMapper.getCameraBeanList(null);

 

         则实际执行的SELECT语句为:

 

SELECT id,title,price FROM Camera ORDER BY id

 

 

2-4-6 分页查询

         所谓的分页查询,是指根据指定的页号、每页行数,以及附加的查询条件,从大量的数据中提取出其中的一部分成为分页的结果集。与分页相关的参数包括页号、每页行数、总行数、总页数等。

 

         总体来说,在MySQL下有三种分页方式:通过SqlSession的方法实现分页、通过分页查询语句实现分页,以及通过自定义拦截器实现分页。

●通过SqlSession的方法实现分页

         首先,<select>语句方面并没有什么变化,还是像往常一样书写即可。例如:

        

<select id="getEmpList" resultType="Emp">                

         SELECT empno,ename,job,hireDate,sal FROM EMP ORDER BY empno             

</select>

 

         接下来,可以通过SqlSession对象的selectList()方法获取分页结果列表。假设每页5行数据,要提取第二页的数据,则调用的代码可以写成:

        

List<Emp> emps=session.selectList("mapper.EmpMapper.getEmpList",null,

                                                                 new RowBounds(5,5));

 

         在这里调用的selectList()方法一共接收三个参数,第一个参数是SELECT语句的id;第二个参数是要绑定到语句中的参数,如果语句没有设置占位符则可以传null;第三个参数是一个RowBounds对象,该对象描述了从结果集提取行数据的范围。

 

         RowBounds的构造函数有两个参数,第一个是“offset”,即偏移量,指前面要略过的行的数量;第二个参数是“limit”,指要提取的最大行数。由于我们要查询第二页的数据,所以要略过第一页的5行数据,然后提取第2页的5行数据,因此在这里将offset和limit都设置为5。以此类推,如果要查询第三页的数据,则应当new RowBounds(10,5)。

 

         当然,这里只演示了查询分页列表的方法。如果还想获得总行数,需要相应的聚合查询,另行调用获取。

 

         这种分页方式问题比较明显。首先,必须通过SqlSession的API来调用查询,不支持Mapper方式调用;其次,在执行selectList()时,会通过映射的SELECT语句先取回全部结果集,然后再在内存中进行遍历和提取。这样做效率非常低下;第三,分页查询的结果往往不只有数据列表,还包括总行数、总页数等参数,而这些参数还需要通过其他的查询来获取和计算。

 

         综上所述,这种分页方式虽然是MyBatis内置的,但并不可取。

●通过分页查询语句实现分页

         第二种分页方式,我们需要自己编写查询总行数和分页列表的SQL语句。首先是查询总行数的语句:

        

<select id="getEmpCount" resultType="_int">

         SELECT COUNT(0) FROM EMP;

</select>

 

         接下来是分页查询语句。不同数据库的分页查询语句写法有差异,下面以Oracle数据库为例:

        

SELECT t.* FROM

(

         SELECT empno,ename,job,hireDate,sal,rownum rn FROM EMP ORDER BY empno

)  t

WHERE t.rn BETWEEN #{first} AND #{last}

 

         上面两个查询对应的Mapper接口方法分别为:

        

public int getEmpCount();

public List<Emp> getEmpPage(@Param("first") int first,@Param("last") int last);

 

         我们还创建一个辅助类,该辅助类用来封装分页查询的结果:

 

public class PageList<T> extends ArrayList<T>{

 

         public PageList(){

                  super();

         }

        

         public PageList(Collection<T> collection){

                  super(collection);

         }

        

         private int pageIndex;

         private int pageSize;

         private int rowCount;

        

         public int getPageCount() {

                  return (rowCount/pageSize)+(rowCount%pageSize==0?0:1);

         }

         //其他的getter和setter方法代码省略

}

 

         在这里我们的PageList类继承了ArrayList类,在具有List集合功能的同时,扩展了pageIndex、pageSize和rowCount等属性,并且通过getPageCount()动态计算并返回总页数。


 

         接下来我们就来看一下一个完整的分页查询方法的代码:

        

public PageList<Emp> find(int pageIndex,int pageSize){

         PageList<Emp> empPage=null;

         SqlSession session=MyBatisUtil.openSession();

         try {

                  EmpMapper empMapper=session.getMapper(EmpMapper.class);

                  //查询总行数

                  int rowCount= empMapper.getEmpCount();

                  int first=(pageIndex-1)*pageSize+1;

                  int last=pageIndex*pageSize;

                  //查询分页结果列表

                  List<Emp> emps= empMapper.getEmpPage(first, last);

                  //封装分页结果

                  empPage=new PageList<Emp>(emps);

                  empPage.setPageIndex(pageIndex);

                  empPage.setPageSize(pageSize);

                  empPage.setRowCount(rowCount);

                 

         } catch (Exception e) {

                  e.printStackTrace();

         }

         finally{

                  session.close();

         }               

         return empPage;

}

 

         当我们需要执行分页查询时,只需调用find()方法,传递页号和每页行数即可。

        

         由于分页结果是通过我们自己编写的分页查询语句获取的,因此与前面的通过调用SqlSession的selectList()方法实现分页的方式相比性能有了很大的提升。但是执行完整的分页查询操作,代码显得有些臃肿;而且如果一个项目中如果有很多数据需要分页显示,那么我们就要映射大量的COUNT聚合查询语句和分页查询语句,产生大量的冗余代码。那么还有没有更好的分页查询方法吗?

●通过自定义拦截器实现分页

         MyBatis中引入了拦截器的概念。所谓拦截器,与JSP/Servlet中的过滤器(Filter)有些类似,可以在我们执行特定的操作前后做一些自定义的操作。所以我们可以通过自定义拦截器来实现MyBatis分页。相关的源代码在网上能找到很多,在这里我们我们进行了一些改造和封装,并导出成jar包。大家只要将jar包导入项目,然后进行相关的配置即可。由于相关代码量较大,因此就不在这里展示,如果想了解实现细节可以看附件中的源码。

         下面我们就重点来说一下有哪些主要的类和接口。这些类和接口都在bdqn.mybatis.plugin包下。

        

PaginationInterceptor

分页拦截器,实现自动分页查询的核心类

PageParam

分页参数,在调用分页查询时通过PageParam传递页号和每页行数,查询结束后,总行数和总页数也会存回pageParam对象中

IDialect

数据库方言接口,定义了创建COUNT聚合查询语句和分页查询语句的操作

AbstractDialect

抽象数据库方言,提供了创建聚合COUNT查询语句的默认实现

OracleDialect

针对ORACLE数据库的方言类。如果需要针对其他数据库的方言类,可以参考OracleDialect的源代码自行扩展

 

         下面我们来看一下如何在MyBatis中配置和使用这个分页拦截器。首先是在MyBatis的配置文件中,添加下列片段:

        

<plugins> 

    <plugin interceptor="cn.bdqn.mybatis.plugin.PaginationInterceptor">

             <property

name="dialectClass" value="cn.bdqn.mybatis.plugin.OracleDialect"/>

     </plugin> 

</plugins>

 

         <plugins>元素用来添加插件,插件是一种对MyBatis的功能进行扩展的机制。在这里我们将PaginationInterceptor配置为一个插件。同时通过<property>元素设置了一个名为dialectClass的属性,属性值就是我们使用的方言类的完整类名。注意:<plugins>元素在MyBatis配置文件中应放在< environments>之前(紧挨着)。

 

         在写SQL语句时,我们并不需要加入任何的分页语法,像往常那样就可以:

 

<select id="getEmpList" resultType="Emp">                

         SELECT empno,ename,job,hireDate,sal FROM EMP ORDER BY empno             

</select>

 

         但是在编写对应的Mpper接口方法时,我们必须传递一个PageParam类型的参数,并将其命名为"pageParam":

        

public List<Emp> getEmpList(@Param("pageParam")PageParam pageParam);

 

         调用时,我们只需要通过pageParam对象传递页号和每页行数即可:

 

EmpDAO empDAO=session.getMapper(EmpDAO.class);

PageParam pageParam=new PageParam(1,5);

List<Emp> emps=empDAO.getEmpList(pageParam);

         PageParam构造函数的第一个参数是页号,第二个参数是每页行数。当前页的结果列表通过方法的返回值返回,而总行数和总页数被存回到pageParam对象中。下面我们可以输出它们:

        

System.out.println(pageParam.getRowCount());

System.out.println(pageParam.getPageCount());

 

         到这里,MyBatis的分页查询就介绍完了。综合上述三种分页方式,SqlSession的selectList()方法性能最差,所以不推荐使用;自定义分页查询语句虽然性能较高,但会出现大量的冗余代码,开发效率低;而使用插件扩展的分页查询,既可以保证执行效率,又可以大幅减少代码量,所以推荐使用这种方式进行分页。

2-4-7 调用存储过程

         除了可以映射各种SELECT、INSERT、UPDATE、DELETE语句外,MyBatis还支持对调用存储过程的CallableStatement进行映射。下面我们就来看一些例子。

●调用执行插入操作的存储过程

         首先我们来定义一个向Dept表插入数据的存储过程:

        

CREATE OR REPLACE PROCEDURE usp_add_dept(

    v_deptno DEPT.DEPTNO%TYPE,

    v_dname DEPT.DNAME%TYPE,

    v_loc DEPT.LOC%TYPE

)

AS

BEGIN

    INSERT INTO DEPT(DEPTNO,DNAME,LOC) VALUES(v_deptno,v_dname,v_loc); 

END usp_add_dept;

/

 

         这个存储过程接收三个参数,全部都是输入(IN)参数。下面我们来映射这个存储过程调用。如果存储过程中包含了增、删、改操作,可以使用<update>元素来映射:

        

<parameterMap type="map" id="addDeptParam">

                  <parameter property="deptNo" mode="IN"/>

                  <parameter property="dname" mode="IN"/>

                  <parameter property="loc" mode="IN"/>

</parameterMap>

<update id="callAddDept" parameterMap="addDeptParam" statementType="CALLABLE">

                  {call usp_add_dept(?,?,?)}

</update>

         首先,MyBatis各种语句的statementType默认为STATEMENT,即在执行时会使用PreparedStatement来处理。但是现在我们要通过CallableStatement来执行调用存储过程的call语句,因此需要显式地将statementType设置为“CALLABLE”。

 

         其次,我们要编写调用存储过程的call语句。在这里有两种写法。第一种写法是将各个参数占位符直接写成“?”:

 

         {call usp_add_dept(?,?,?)}

 

         这样,我们必须通过外部的<parameterMap>元素来描述参数对象类型,以及参数对象属性名(键)与各个占位符的对应关系:

        

<parameterMap type="map" id="addDeptParam">

         <parameter property="deptNo" mode="IN"/>

         <parameter property="dname" mode="IN"/>

         <parameter property="loc" mode="IN"/>

</parameterMap>

 

         在这里,我们将参数类型设置为map集合,然后通过<parameter>元素定义集合中的各个元素与占位符的对应关系。注意: <parameter>元素的排列顺序要与存储过程的参数顺序一致。例如,我们要调用的usp_add_dept存储过程的三个参数分别为部门编号、部门名称和所在地,那么<parameter>也要按这个顺序排列。

 

         第二种写法是直接将各个参数的映射信息卸载参数占位符中,这样就不需要定义外部的<parameterMap>元素了:

 

<update id="callAddDept" parameterType="map" statementType="CALLABLE">             

         {

                  call usp_add_dept(

                          #{deptNo,mode=IN},

                          #{dname},

                          #{loc}

                  )

         }

</update>

 

         在占位符中,我们可以通过mode=IN表示该参数为输出参数。当然,默认的参数类型就是IN,所以也可以省略mode,直接将占位符和参数对象的属性名(键)对应。

        

         接下来我们来看一下调用这个存储过程的Mapper接口的方法声明:

        

public void callAddDept(Map<String,Object> dept);

 

         下面我们来看一下调用方法的代码:

SqlSession session=MyBatisUtil.openSession();

EmpMapper empMapper=session.getMapper(EmpMapper.class);

try {

         Map<String,Object> dept=new HashMap<String,Object>();

         dept.put("deptNo",70);

         dept.put("dname","物流部");

         dept.put("loc","北京");

         empMapper.callAddDept(dept);

         session.commit();

} catch (Exception e) {

         session.rollback();

}

finally{

         session.close();

}

        

         在这里我们首先创建一个Map集合对象,将要传递给存储过程的参数放入集合。注意在调用put()方法时,key要与语句中的参数占位符一致。同时,由于是添加数据操作,在这里使用了事务控制。

 

●调用带输出参数的存储过程

         存储过程还可以通过输出参数返回一个或多个结果。下面我们来创建一个根据部门编号返回部门总工资、平均工资、最高工资和最低工资的存储过程:

        

CREATE OR REPLACE PROCEDURE usp_dept_sal(

     v_deptno EMP.DEPTNO%TYPE,

     v_total_sal OUT EMP.SAL%TYPE,

     v_avg_sal OUT EMP.SAL%TYPE,

     v_max_sal OUT EMP.SAL%TYPE,

     v_min_sal OUT EMP.SAL%TYPE

)

AS

BEGIN

      SELECT SUM(sal),AVG(sal),MAX(sal),MIN(sal)

         INTO v_total_sal,v_avg_sal,v_max_sal,v_min_sal

      FROM emp WHERE deptno=v_deptno;

END usp_dept_sal;

 

         在这个存储过程中,第一个参数v_deptno,即部门编号是输入参数,而其余的都是输出参数。下面我们来看一下在MyBatis中如何映射这个存储过程调用:

 

<select id="callDeptSal" parameterType="map" statementType="CALLABLE">

         {

                  call usp_dept_sal(

                          #{deptNo,mode=IN},

                          #{totalSal,jdbcType=DOUBLE,mode=OUT },

                          #{avgSal,jdbcType=DOUBLE,mode=OUT,javaType=java.lang.Double},

                          #{maxSal,jdbcType=DOUBLE,mode=OUT,javaType=java.lang.Double},

                          #{minSal,jdbcType=DOUBLE,mode=OUT,javaType=java.lang.Double}

                  )

         }

</select>

 

         第一,由于这个存储过程不会对数据进行修改,因此我们可以使用<select>元素来映射。

 

         第二,我们将parameterType设置为map,即调用这个语句时传入的参数是一个Map集合。这个Map集合不仅用来在调用时传入输出参数,在调用存储过程后各个输出参数的值会也会被存入这个map集合中。

 

         第三,我们依然需要将语句的statementType设置为CALLABLE。

 

         第四,设置展位符时,如果是输出参数,必须显式指定jdbcType和mode,javaType是可选的。在这里jdbcType有以下选项:

 

BIT

FLOAT

CHAR

TIMESTAMP

OTHER

UNDEFINED

TINYINT

REAL

VARCHAR

BINARY

BLOB

NVARCHAR

SMALLINT

DOUBLE

LONGVARCHAR

VARBINARY

CLOB

NVARCHAR

INTEGER

NUMERIC

DATE

LONGVARBINARY

BOOLEAN

NCLOB

BIGINT

DECIMAL

TIME

NULL

CURSOR

 

 

         对应的Mapper接口方法如下:

 

public void callDeptSal(Map<String,Object> params);

 

         下面我们来看一下测试代码:

        

EmpMapper empMapper=session.getMapper(EmpMapper.class);

Map<String,Object> params=new HashMap<String, Object>();

params.put("deptNo",10);

empMapper.callDeptSal(params);

Double totalSal=(Double)params.get("totalSal");

Double avgSal=(Double)params.get("avgSal");

Double maxSal=(Double)params.get("maxSal");

Double minSal=(Double)params.get("minSal");

         我们首先创建一个Map集合params,将输入参数“deptNo”存入集合,调用callDeptSal()方法后,就可以从params中取出各个输出参数的返回值了。

●调用返回结果集的存储过程

         Oracle数据库可以通过SYS_REFCURSOR类型的输出参数返回结果集。下面我们就来编写一个根据部门编号返回员工列表的存储过程:

 

CREATE OR REPLACE PROCEDURE usp_get_emps(

    v_deptno EMP.DEPTNO%TYPE,

    v_emps OUT SYS_REFCURSOR

)

AS

BEGIN

   OPEN v_emps FOR

   'SELECT empno,ename,job,hireDate,sal,rownum rn

   FROM EMP WHERE deptno=:deptno ORDER BY empno'

   USING v_deptno;

END usp_get_emps;

 

         SYS_REFCURSOR是一个系统定义的弱类型动态游标类型,我们用它声明了输出参数v_emps,并且在存储过程的执行部分使用了OPEN 指令动态地执行了SELECT语句,打开了游标。

 

         下面我们来看一下如何在MyBatis中映射这个存储过程调用:

        

<resultMap type="Emp" id="EmpMap">

         <id column="empNo" property="empNo"/>

         <result column="ename" property="ename"/>

         <result column="job" property="job"/>

         <result column="hireDate" property="hireDate"/>

         <result column="sal" property="sal"/>

</resultMap>

 

<select id="callGetEmps" parameterType="map" statementType="CALLABLE">

         {

                  call usp_get_emps(

                          #{deptNo,mode=IN},             

                           #{emps,jdbcType=CURSOR,mode=OUT, resultMap=EmpMap}

                  )

         }

</select>

 

       首先,为了映射游标返回的结果集与实体类的对应关系,我们需要在外部设置一个<resultMap>元素。其次,在于游标输出参数对应的占位符中,需要将jdbcType设置为CURSOR,并且通过resultMap指定使前面定义的结果集映射规则。

 

         下面是测试代码:

        

EmpMapper empMapper=session.getMapper(EmpMapper.class);

Map<String,Object> params=new HashMap<String, Object>();

params.put("deptNo",10);

empMapper.callGetEmps(params);

List<Emp> emps=(List<Emp>)params.get("emps");

for(Emp emp:emps){

         System.out.println(emp.getEname());

}               

 

         我们先创建一个Map集合params,将要传递给输入参数“deptNo”的值存入,在调用了callGetEmps()方法后,就可以从params中取出封装好的List集合。

 

第三章性能优化

3-1 缓存

         如果我们每次查询数据时都要通过数据库连接执行SELECT指令,在访问量较大、操作教密集时,会加大系统开销、影响系统性能、降低程序的响应速度。因此一个比较好的做法是将结果缓存起来,如果下一次执行相同的查询,则直接返回缓存中的数据。这样可以降低与数据库通信的频率,在一定程度上提升系统性能。MyBatis中有两种缓存,我们称之为一级缓存和二级缓存。

3-1-1 一级缓存

         我们每次通过SqlSessionFactory的openSession()方法获取一个SqlSession对象,其内部就包含了一个缓存,我们称之为一级缓存。如果我们通SqlSession对象调用的查询,其结果会被置入一级缓存。如果接下来我们通过同一个SqlSqlSession对象执行相同的查询,则直接返回一级缓存中的结果,而不会再次查询数据库。下面我们来看一个例子。首先,添加一个根据id查询单条Emp记录的SELECT语句:

        

<select id="getEmp" resultType="Emp">

         SELECT empno,ename,job,hireDate,sal FROM EMP WHERE empno=#{id}

</select>

 

         调用这条语句的Mapper接口方法是:

 

public Emp getEmp(long id);

 

         接下来我们来看一下测试代码:

 

SqlSession session=MyBatisUtil.openSession();

EmpMapper empMapper=session.getMapper(EmpMapper.class);

Emp emp1= empMapper.getEmp(7369);

System.out.println("emp1.ename="+emp1.getEname());

Emp emp2= empMapper.getEmp(7369);

System.out.println("emp1.ename="+emp2.getEname());

session.close();

 

         在上面的代码中,我们先创建了一个SqlSession对象,然后分别调用了两次getEmp方法,传递的参数都是7369。执行上面的代码,我们会看到下面的日志输出:

        

==>  Preparing: SELECT empno,ename,job,hireDate,sal FROM EMP WHERE empno=?

==> Parameters: 7369(Long)

<==    Columns: EMPNO, ENAME, JOB, HIREDATE, SAL

<==        Row: 7369, SMITH, CLERK, 1980-12-17 00:00:00, 800

emp1.ename=SMITH

emp1.ename=SMITH

......

 

         在这里,第一次调用getEmp()方法时,执行了SELEC语句,而最后一次调用getEmp()方法,并传递相同的参数时,就不再执行SELECT语句,而是直接返回了缓存的Emp对象。

 

         而如果我们两次查询使用的是不同的参数,或者使用的是不同的Session对象,则会看到如下的日志输出:

        

==>  Preparing: SELECT empno,ename,job,hireDate,sal FROM EMP WHERE empno=?

==> Parameters: 7369(Long)

<==    Columns: EMPNO, ENAME, JOB, HIREDATE, SAL

<==        Row: 7369, SMITH, CLERK, 1980-12-17 00:00:00, 800

emp1.ename=SMITH

ooo Using Connection [oracle.jdbc.driver.T4CConnection@1b980630]

==>  Preparing: SELECT empno,ename,job,hireDate,sal FROM EMP WHERE empno=?

==> Parameters: 7782(Long)

<==    Columns: EMPNO, ENAME, JOB, HIREDATE, SAL

<==        Row: 7782, CLARK, MANAGER, 1981-06-09 00:00:00, 2450

emp1.ename=CLARK

 

         可见,SQL语句执行了两次。

3-1-2 二级缓存

         在上一节中我们看到,每一个SqlSession对象有一个内置的一级缓存。也就是说这个缓存中的数据只能在在对同一个SqlSession对象进行操作时才能使用。那么怎么才能让缓存的数据可以被所有的SqlSession对象用到呢?这就需要配置MyBatis的二级缓存。

 

         要启用二级缓存,只需要在Sql映射文件中加入<cache>元素即可,如下所示:

 

<?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="bdqn.mybatis.dao.EmpDAO">

         <cache/>

    ......

        

         添加<cache>元素后,该Sql映射文件中的语句将启用二级缓存。其他Sql映射文件如果也要使用二级缓存,则也应当添加<cache>元素。默认地,二级缓存具有以下特性:

        

l  执行该Sql映射文件中的SELECT语句返回的所有结果都将被缓存

l  执行该Sql映射文件中的INSERT、UPDATE和DELETE语句,将刷新缓存

l  二级缓存将使用最近最少使用(LRU)算法进行回收

l  不会定期对缓存进行刷新,即只有对数据进行增删改操作时才刷新缓存

l  缓存将存入1024个由查询方法返回的列表或对象的引用

l  缓存将视为读/写(read/write)缓存,意味着从缓存中取出的对象可以安全地被调用者修改,而不会影响其他线程对缓存对象的操作。

        

         以上各项默认特性都可以通过<cache>元素的各个参数进行重新设定。如下所示:

 

属性名

说明

eviction

回收算法。可选值包括:

LRU(最近最少使用):是默认算法。该算法会回收长期未被使用的缓存对象

FIFO(先进先出):按对象进入缓存的先后顺序进行回收,先进入缓存的对象先被回收

SOFT:通过软引用(Soft Reference)机制保存对象。即:当执行垃圾回收(gc)时,如果内存空间够用,则对象将被保留;当内存空间不足时,对象将被释放

WEAK:通过弱引用(Weak Reference)机制保存对象。即:当执行垃圾回收时,无论内存空间是否够用,都将回收对象。

flushInterval

执行缓存清理的时间间隔,单位为毫秒。

size

缓存中保存的对象数量,默认为1024

readOnly

缓存是否只读。如果该选项设置为true,则表示缓存是只读的。此时不同的调用者从缓存中取出的时同一个对象的引用。如果不需要对取出的缓存对象进行修改,则可以将缓存设置为只读的,以换取较好的性能。反之,如果需要对取出的对象进行修改,则应将该选项设置为false,即表示缓存是读写的。该选项默认为false。

 

         注意:二级缓存是事务的。即:当SqlSessin对象通过commit提交或通过rollback回滚(且没有执行过flushCache=true的insert/delete/update语句)时,缓存将被更新。下面我们来看一个例子:

        

<cache

         eviction="FIFO"

         flushInterval="60000"

         size="512"

         readOnly="true"/>

 

         上面的例子中,我们设置了二级缓存以先进先出(FIFO)算法进行回收,每个1分钟清理一次缓存,最多保存512个对象,且是只读的。接下来看一个完整案例。首先是SQL映射文件:

        

.......

<cache/>

 

<select id="getEmpList" resultType="Emp">                

         SELECT empno,ename,job,hireDate,sal FROM EMP ORDER BY empno             

</select>

......

 

         对应的Mapper接口方法如下:

        

public List<Emp> getEmpList();

 

         接下来是测试代码:

        

SqlSession session1=MyBatisUtil.openSession();

EmpMapper empMapper=session1.getMapper(EmpMapper.class);

List<Emp> emps=empMapper.getEmpList();

//遍历代码省略.....

Session1.close();

SqlSession session2=MyBatisUtil.openSession();

EmpMapper empMapper2=session2.getMapper(EmpMapper.class);

List<Emp> emps2=empMapper2.getEmpList();

//遍历代码省略......

session2.close();

         在上面的代码中,我们先后创建了两个SqlSession对象,并且分别通过这两个SqlSession对象获得了Mapper对象。运行代码后,当在session1中执行getEmpList()方法时,会执行SELECT语句,而当在session2中执行getEmpList()方发时,SELECT语句就不再执行,并且会看到如下日志输出:

 

Cache Hit Ratio [mapper.EmpMapper]: 0.5

 

         该日志输出的是缓存命中率(CacheHit Ratio)。      所谓命中,是指如果我们根据特定的键(key)可以从缓存中取出对象时,则为命中。命中率的计算公式为:

        

         命中率=命中次数/请求次数

 

         由于我们通过添加<cache>元素启用了二级缓存,所以当我们第一次调用getEmpList()方法时,会先向缓存请求(request)获取对象,而此时缓存时空的,因此第一次没有命中。而在二次调用getEmpList()方法,再次向缓存请求获取对象时,此时缓存中已经保存了第一次查询到的结果,可以取出对象,即“命中”了缓存。我们一共请求了两次,命中了一次,命中率为0.5,即50%。

        

         我们可以通过在MyBatis配置文件进行配置,在全局上启用或者禁用二级缓存:

        

<settings>

        ......

         <setting name="cacheEnabled" value="false"/>

         ......

</settings>

 

         如果将cacheEnabled选项设置为false,则表示禁用二级缓存。

3-2 延迟加载

         当我们在<resultMap>中通过<association>或<collection>映射关联的对象或集合时,可以通过select元素设置其以独立查询的方式加载。若如此做,即可对该关联对象或集合启用延迟加载策略。即当我们真的需要使用关联对象或集合中的数据时,才执行SELECT加载数据。要启用延迟加载,需要在MyBatis的配置文件中添加以下设置:

        

<settings>

         ......

         <setting name="lazyLoadingEnabled" value="true" />                

         ......

</settings>     

 

         lazyLoadingEnabled属性表示是否启用延迟加载。

        

         下面我们来看一个例子。首先是实体类:

        

public class Emp implements Serializable{

         private int empNo;

         private String ename;

         private String job;

         private Date hireDate;

         private double Sal;

         private Double Comm;

         private Dept dept;

         //getter和setter方法省略

}

 

public class Dept implements Serializable{

         private int deptNo;

         private String dname;

         private String loc;

         //getter和setter方法省略

}

 

         在EMP类中,通过dept属性保存员工关联的部门(Dept)对象。

 

         接下来我们在SQL映射文件中编写查询及其结果映射:

 

<resultMap type="Emp" id="EmpMap">

         <id column="empno" property="empNo"/>

         <result column="ename" property="ename"/>

         <result column="job" property="job"/>

         <result column="hireDate" property="hireDate"/>

         <result column="sal" property="sal"/>

         <association property="dept" column="deptno" select="getDept"/>

</resultMap>

 

<select id="getEmp" resultMap="EmpMap">

         SELECT empno,ename,job,hireDate,sal,deptno FROM EMP WHERE empno=#{id}

</select>

 

<select id="getDept" resultType="Dept">

         SELECT deptno,dname,loc FROM Dept WHERE deptno=#{id}

</select>

 

         在<resultMap>中,通过<association>元素的select属性设置了通过id为“getDept”的查询来加载关联的Dept对象,column为外键列名,该列的值将被传递给“getDept”查询并绑定到#{id}占位符上。

         接下来我们来看一下程序的调用代码:

        

SqlSession session=MyBatisUtil.openSession();

EmpMapper empMapper=session.getMapper(EmpMapper.class);

Emp emp=empMapper.getEmp(7369);

session.close();

System.out.println("session关闭后,获取部门名称");

System.out.println(emp.getDept().getDname());

 

         执行这段代码,将看到如下日志输出:

 

......

session关闭后,获取部门名称

Opening JDBC Connection

ooo Using Connection [oracle.jdbc.driver.T4CConnection@56ad2c30]

==>  Preparing: SELECT deptno,dname,loc FROM Dept WHERE deptno=?

==> Parameters: 20(BigDecimal)

<==    Columns: DEPTNO, DNAME, LOC

<==        Row: 20, RESEARCH, DALLAS

......

        

         可见,当我们调用emp.getDept().getDname()方法时,才执行了加载部门数据的SELECT语句。这就是延迟加载。

3-2-1 侵略性延迟加载

         将上面的代码稍微改造一下:

 

......

session.close();

System.out.println("session关闭后,获取员工姓名:");

System.out.println(emp.getEname());

System.out.println("session关闭后,获取部门名称:");

System.out.println(emp.getDept().getDname());

 

         我们加入了一行调用emp.getEname()方法输出员工姓名的代码。再次执行查询,我们会看到如下日志输出:

 

session关闭后,获取员工姓名:

Opening JDBC Connection

ooo Using Connection [oracle.jdbc.driver.T4CConnection@56ad2c30]

==>  Preparing: SELECT deptno,dname,loc FROM Dept WHERE deptno=?

......

         说明我们在获取员工姓名时,就已经执行了加载部门数据的查询。实际上这时我们并没有表示要使用部门数据。这时因为MyBatis默认采用了“侵略性延迟加载策略”,即访问对象的任何属性,都将引发关联对象或集合的加载。这实际上与不配置延迟加载没有什么区别了。因此,我们往往还需要在MyBatis配置文件中设置另一个参数:

        

<settings>

         ......

         <setting name="lazyLoadingEnabled" value="true" />

         <setting name="aggressiveLazyLoading" value="false" />              

         ......

</settings>

 

         aggressiveLazyLoading表示是否启用侵略性延迟加载策略,false表示禁用。

3-3 连接池

         MyBatis提供了内置的连接。要启用连接池机制,需要在MyBatis的配置文件中将数据源类型设置为"POOLED",如下所示:

 

<environments default="dev">

         <environment id="dev">

                  <transactionManager type="JDBC"/>

                  <dataSource type="POOLED">                               

                                   <property name="driver" value="oracle.jdbc.driver.OracleDriver"/>

                                   <property name="url"

                                                     value="jdbc:oracle:thin:@localhost:1521:ORCL"/>

                                   <property name="username" value="scott"/>

                                   <property name="password" value="ok"/>

                                   <property name="poolMaximumActiveConnections" value="20"/>

                                   <property name="poolMaximumIdleConnections" value="10"/>

                                   <property name="poolMaximumCheckoutTime" value="2000"/>

                                   <property name="poolTimeToWait" value="20000"/>

                                   <property name="poolPingQuery" value="SELECT 1=1 FROM DUAL"/>

                                   <property name="poolPingEnabled" value="true"/>

                                   <property name="poolPingConnectionsNotUsedFor" value="0"/>

                          </dataSource>

         </environment>

</environments>

 

         可以看到,配置POOLED类型的数据源时,除了driver、url、username和password等几个基础参数外,还设置了一些和连接池相关的参数:

 

poolMaximumActiveConnections

最大活(使用中)动连接数,默认为10个。

poolMaximumIdleConnections

最大空闲连接数

poolMaximumCheckoutTime

一个被“签出”的连接可以使用的最长时间,超出此时间将被强制返回。单位:毫秒,默认为2000毫秒。

poolTimeToWait

当请求连接时,如果所有连接均为活动状态,且使用时间均未超出“poolMaximumCheckoutTime”设置的最长使用时间,将打印日志状态并尝试重新获取连接。等待时间单位为毫秒,默认为20000毫秒。

poolPingQuery

向数据库发送的“Ping”查询语句,以验证连接是有效。该参数的默认值为“NO PING QUERY SET”。对于大多数驱动,执行这样的查询将导致错误发生。

poolPingEnabled

启用或禁用PING查询。如果启用,必须同时设置poolPingQuery参数。默认为false。

poolPingConnectionsNotUsedFor

这个参数用来设置多长时间使用一次poolPingQuery查询。可以设置为与数据库的连接超时时间一致,以避免无谓的检查。单位:毫秒。默认为0,表示每次获取连接时都执行检查。该参数起效的前提是将poolPintEnabled设置为true。