框架 - mybatis(动力节点:老杜从零学mybatis入门到架构思维)(二)
八、MyBatis小技巧
-
使用#{}和${}的区别:
-
#{}:底层使用PreparedStatement。先进行SQL语句的编译,然后给SQL语句的占位符问号?传值。可以避免SQL注入的风险。
-
${}:底层使用Statement。先进行SQL语句的拼接,然后再对SQL语句进行编译。存在SQL注入的风险。
-
所以一般会使用#{},${}只有某些特定的场景才会使用。总结一下两者的根本区别就是#{}用于使用PreparedStatement给?传值,${}用于直接拼接sql语句。
-
-
有些地方可以使用${},体会下列例子
-
场景:查询所有的账户列表,通过指定的排序模式,依据账户余额进行升序或者降序排序。
-
编写dao接口:
public interface AccountDao { // String descOrEsc表示升序或者降序排序 List<Account> selectAll(String descOrEsc); } -
编写AccountMapper.xml文件:
<mapper namespace="shh.dao.AccountDao"> <select id="selectAll" resultType="shh.pojo.Account"> select * from t_account order by balance #{descOrEsc}; </select> </mapper> -
执行测试:执行报错
public static void main(String[] args) { AccountDao accountDao = SqlSessionUtil.getSqlSession().getMapper(AccountDao.class); List<Account> desc = accountDao.selectAll("desc"); System.out.println(desc); }分析:使用#{}之后,底层使用prepareStatement。首先得到sql:select * from t_account order by balance ?,进行拼接之后就是:select * from t_account order by balance 'desc',自动添加了单引号,所以执行报错。
-
改用${}即可执行成功
<mapper namespace="shh.dao.AccountDao"> <select id="selectAll" resultType="shh.pojo.Account"> select * from t_account order by balance ${descOrEsc}; </select> </mapper>分析:拼接之后的sql是:select * from t_account order by balance desc,可以成功执行。
-
-
需要在sql中拼接表名的时候:现实业务当中,可能会存在分表存储数据的情况。因为一张表存的话,数据量太大。查询效率比较低。可以将这些数据有规律的分表存储,这样在查询的时候效率就比较高。因为扫描的数据量变少了。
例如日志表:专门存储日志信息的。如果t_log只有一张表,这张表中每一天都会产生很多log,慢慢的,这个表中数据会很多。怎么解决问题?可以每天生成一个新表。每张表以当天日期作为名称,例如:
t_log_20220901、t_log_20220902-
你想知道某一天的日志信息怎么办?假设今天是20220901,那么直接查:t_log_20220901的表即可。那么查询某天的日志信息的时候,就需要找到某一天对应的表,然后将表名拼接到sql中。
-
这个时候如果使用#{},那么就会出现上一个例子中出现的异常。所以就需要使用${}来拼接sql
<mapper namespace="shh.dao.AccountDao"> <select id="selectAll" resultType="shh.pojo.Account"> select * from ${tableName}; </select> </mapper>
-
-
一次性删除多条数据:删除多条数据,要么使用or,要么使用in操作,所以拼接sql的时候也可能会发生错误,所以会使用${}。
-
模糊查询:like
-
需求:根据汽车品牌进行模糊查询:select * from t_car where brand like '%奔驰%';
-
第一种方案:'%${brand}%'(这里面就不能使用#{},因为编译之后是这样的:'%?%',prepareStatement无法给单引号中的?传值。)
-
第二种方案:concat函数,这个是mysql数据库当中的一个函数,专门进行字符串拼接
concat('%',#{brand},'%') -
第三种方案:"%"#{brand}"%"(使用#{},因为编译之后是这样的:%?%,?不在单引号里面了,prepareStatement可以给?传值。)
-
-
-
别名机制
-
在XxxMapper.xml文件中,经常需要执行参数类型,返回值数据类型,这个数据类型一般会比较长,例如以下,就是:shh.dao.AccountDao,非常不方便,所以mybatis提供了别名机制。
<mapper namespace="shh.dao.AccountDao"> <select id="selectByNo" resultType="shh.pojo.Account"> select * from t_account where actno = #{no}; </select> <select id="selectAll" resultType="shh.pojo.Account"> select * from t_account order by balance ${descOrEsc}; </select> </mapper> -
为某个类执行别名:在mybatis-config.xml核心配置文件中,可以进行以下配置,为某个类起别名:
<typeAliases> <!--type指的是要起别名的类名,alias表示要起的别名。起别名之后,使用别名时不区分大小写--> <typeAlias type="shh.pojo.Account" alias="account"></typeAlias> </typeAliases> -
可以使用mybatis的默认别名机制:
<typeAliases> <!--使用mybatis的默认别名机制,默认的别名就是不带包名的类名,不区分大小写.--> <typeAlias type="shh.pojo.Account"></typeAlias> </typeAliases> -
还有更方便的起别名方式:执行某个包,该包下的所有类都会使用默认别名机制起别名。
<typeAliases> <!-- 这样一来,这个包下面的所有类都会使用默认别名. --> <package name="shh.pojo"/> </typeAliases>
-
-
mybatis-config.xml文件中的mappers标签。
-
mappers标签中的mapper标签有三种指定mapper路径的方式:
<mapper resource="CarMapper.xml"/> 要求类的根路径下必须有:CarMapper.xml <mapper url="file:///d:/CarMapper.xml"/> 要求在d:/下有CarMapper.xml文件 <mapper class="全限定接口名,带有包名"/>-
resource:这种方式是从类的根路径下开始查找资源。采用这种方式的话,你的配置文件需要放到类路径当中才行。
-
url:这种方式是一种绝对路径的方式,这种方式不要求配置文件必须放到类路径当中,哪里都行,只要提供一个绝对路径就行。这种方式使用极少,因为移植性太差。
-
class:这个位置提供的是mapper接口的全限定接口名,必须带有包名的。
-
-
思考:mapper标签的作用是指定SqlMapper.xml文件的路径,使用class属性指定接口名有什么用呢?
<mapper class="com.powernode .mybatis.mapper.CarMapper"/> 如果你class指定是:com.powernode.mybatis.mapper.CarMapper 那么mybatis框架会自动去com/powernode/mybatis/mapper目录下查找CarMapper.xml文件。 注意: 如果你采用这种方式,那么你必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下。 -
但是在mappers中还有另一个可用标签:packet,这也是我们最常用的一种方式。需要注意的是如果使用这种方式,XML文件必须和接口放在一起。并且名字一致。否则会找不到配置文件而报错。
<mappers> <!--前提是:XML文件必须和接口放在一起。并且名字一致。--> <package name="指定接口存放的包路径"/> </mappers>
-
-
idea中可以添加文件模板。例如添加一个mybatis-config.xml的模板,下次需要mybatis-config.xml的时候,就可以直接使用模板创建出来了。(略过,要的时候再查)
-
获取自动生成的主键值
-
场景:有一张表A,插入数据时会自动生成主键值id。有另一张表B,与表A关联,当在A中插入数据X的时候,需要在表B中存入相应的数据X的id值。那么就需要在在表A中插入数据X的同时获取到X的id值。
-
编写Dao接口:
public interface AccountDao { // account对象用于传递需要插入表中的数据参数,也用于接收生成的id值。 public void insertAccount(Account account); } -
编写xxxMapper.xml文件
<mapper namespace="shh.dao.AccountDao"> <!-- useGeneratedKeys="true":表示使用自动生成的主键值。 keyProperty="id":指定主键值赋值给对象的哪个属性。这个就表示将主键值赋值给Car对象的id属性。 --> <insert id="insertAccount" useGeneratedKeys="true" keyProperty="id"> insert into t_account values (null,#{actno},#{balance}); </insert> </mapper> -
测试:
public static void main(String[] args) { AccountDao accountDao = SqlSessionUtil.getSqlSession().getMapper(AccountDao.class); Account account = new Account(null, "aaa", 0); // account对象用于传递插入数据搜需要的参数,也用于接收生成的id值. accountDao.insertAccount( account); System.out.println(account); }
-
九、MyBatis参数处理(重要)
-
mybatis中,比较核心的问题有两点,就是参数传递和结果返回。这一章就解决参数处理的问题,下一章解决返回值问题。
-
参数处理问题:也就是研究dao接口中的参数传递到#{}中的问题
-
结果返回:研究封装查询结果的问题。
-
-
单个简单类型参数
-
简单类型是指:基本数据类型以及他们的包装类,String、java.util.Date、java.sql.Date
-
只有单个参数的时候,Dao层接口中的方法中的参数名可以随便写,因为只有一个参数嘛,不会有任何歧义,Mybatis可以很简单地自动进行推断。
-
然后在XxxxMapper.xml文件中,可以通过parameterType属性指定参数类型。这个属性的值可以省略,因为Mybatis可以自动推断参数类型,但是如果加上这个属性的话,性能会高一点,因为mybatis就不需要去解析参数类型了。
<mapper namespace="shh.dao.AccountDao"> <!-- 使用parameterType属性来指定参数类型 --> <select id="selectByName" resultType="student" parameter="java.lang.String"> select * from t_student where name = #{name}; </select> </mapper> -
注意parameterType属性也可以使用别名,也就是上面提到的核心配置文件中的typeAliases标签配置的别名,但是Mybatis中配置了一些内置的别名,可以参考Mybatis官方文档。
-
注意还有另一种声明参数类型的方式:(了解即可,基本上不会用到)
<select id="selectByName" resultType="student"> select * from t_student where name = #{name,javaType=String,jdbcType=VARCHAR} </select>
-
-
Map参数:
-
使用map来传递参数也是可以的,parameterType可以指定为map(内置别名),使用#{map中存放的key值} 来获取map中存放的数据。(在前面演示过了)**
-
也可以不指定参数类型,只有一个参数的话,Mybatis可以自动推断参数类型。
-
-
使用实体类作为参数:
-
使用实体类来传递参数也是可以的,parameterType可以指定为全类名(也可以使用别名),使用#{实体类中的属性名} 来获取map中存放的数据。会自动调用实体类的get方法去获取数据。(在前面演示过了)
-
也可以不指定参数类型,只有一个参数的话,Mybatis可以自动推断参数类型。
-
-
多个参数
-
当有多个参数的时候,mybatis框架底层是怎么做的呢?mybatis框架会自动创建一个Map集合。并且Map集合是以这种方式存储参数的:例如接口方法中参数列表为(String name, char sex)
map.put("arg0", name);
map.put("arg1",sex);
map.put("param1", name);
map.put("param2",sex); -
示例:根据name和sex查询Student信息。
-
编写dao接口
public interface StudentDao { // 有两个参数 age 和 sex Student queryByAgeAndSex(int age,Character sex); } -
如果XxxxMapper.xml使用以下配置:会执行报错,因为找不到age和sex参数
<select id="queryByAgeAndSex" resultType="shh.pojo.Student"> select * from student where age = #{age} and gender = #{sex} </select> -
必须使用mybatis底层定义的map集合存放的键值argX或者paramX,可以混合使用。例如:
<select id="queryByAgeAndSex" resultType="shh.pojo.Student"> select * from student where age = #{arg0} and gender = #{param2} </select>
-
-
-
@Param注解:
-
在上面的多参数场景中,传递参数时必须使用内部定义的Map集合来传递参数,而定义的key值是argX,paramX,这就很不方便,缺乏可读性
-
可以使用@Param("自定义参数名")注解,为Dao接口中的参数指定存放到Map集合中的键值,会替换掉argX这一部分键值,paramX这部分是还在的。
-
使用示例
-
在定义dao接口时:
public interface StudentDao { Student queryByAgeAndSex(@Param("age") int age, @Param("sex") Character sex); } -
在XxxMapper中使用参数键值:
<select id="queryByAgeAndSex" resultType="shh.pojo.Student"> <!-- 必须使用注解中的键值或者ParamX这部分键值 --> select * from students where age = #{age} and gender = #{sex} </select>
-
-
-
@Param注解的源码分析:略
十、MyBatis查询结果专题(重要)
-
返回单个实体类
-
其实就没啥特殊的,在XxxxMapper.xml中使用ResultType指定返回值类型即可(可以使用别名机制)。
-
要注意的是,实体类中的属性属性名必须和表中的属性名一致,否则就会封装不进去实体类中。如果发现实体类和表中属性不一致,可以通过在sql中,对查询到的数据属性起别名来解决。(前面都讲过了,不再重复)
-
-
返回多个查询结果:返回List集合。
-
当需要查询所有,也就是查询得到一个结果集LIst时,需要注意的是,在XxxxMapper.xml中ResultType的值是指定List中保存的数据的类型。
-
例如:查询所有的学生信息
-
编写Dao接口:
public interface StudentDao { List<Student> queryAll(); } -
编写XxxxMapper.xml:
<mapper namespace="shh.dao.StudentDao"> <!-- 注意resultType指定的是List集合保存的数据类型 --> <select id="queryAll" resultType="shh.pojo.Student"> select * from students; </select> </mapper>
-
-
注意:如果查询结果是多条数据的,但是没有使用List集合来接收数据,那么就会报错。
-
-
返回Map
-
当返回的数据,没有合适的实体类对应的话,可以采用Map集合接收。字段名做key,字段值做value。查询如果可以保证只有一条数据,则返回一个Map集合即可。
-
返回一个Map示例:
-
编写Dao接口:
public interface StudentDao { Map<String,Object> queryById(@Param("id") int id); } -
编写XxxMapper.xml文件
<mapper namespace="shh.dao.StudentDao"> <select id="queryById" resultType="java.util.Map"> select * from students where id = #{id}; </select> </mapper>
-
-
返回多个Map示例:其实就是返回一个存储Map的List集合。
-
编写Dao接口:
public interface StudentDao { List<Map<String,Object>> queryAll(); } -
编写XxxMapper.xml文件
<mapper namespace="shh.dao.StudentDao"> <select id="queryAll" resultType="java.util.Map"> select * from students; </select> </mapper>
-
-
在上面的例子中,在查找的结果List<Map<String,Object>>中,如果要获取ID=3的某一个对象的信息,那么就需要遍历List集合,获取Map集合,然后判断当前Map集合中保存的对象是否符合ID=3的条件,很不方便,可以采取另一种方式来保存多个Map:Map<Long,Map<String,Object>> resultMap,保存的数据类似下图,这样去除满足ID=3的某个对象的数据的时候,就可以直接通过Map集合的key值来判断了。
![]()
-
编写Dao接口:需要使用MapKey来指定Map中,key值要保存的属性名。
public interface StudentDao { // 需要使用MapKey来指定Map中,key值要保存的属性名。 @MapKey("id") Map<Long,Map<String,Object>> queryAll(); } -
XxxMapper.xml文件:注意resultType的值。
<mapper namespace="shh.dao.StudentDao"> <select id="queryAll" resultType="java.util.Map"> select * from students; </select> </mapper>
-
-
-
ResultMap结果映射
-
查询结果的列名和java对象的属性名对应不上怎么办?
-
as给列起别名
-
使用resultMap进行结果映射:指定java类中的属性名和表中的属性名的对应关系。
-
开启驼峰命名自动映射(配置settings)
-
-
使用示例:
-
实体类:
public class Student { private Long id; private String n; private Integer a; private Character g; private Date b; } -
表:
![]()
-
dao接口:查询所有的Student数据
public interface StudentDao { public List<Student> queryAll(); } -
分析:可以看到Sutdent类中的属性名和表中的属性名完全不一致。如果依然使用以上展示的方式来实现的话,必然会由于属性名对应不上而报异常。这个时候就需要使用ResultMap映射来解决这个问题了(也可以使用起别名,但是开启驼峰匹配也没有意义了)。
-
XML文件:使用ResultMap来执行实体类和数据库表中的属性映射关系。
<mapper namespace="shh.dao.StudentDao"> <!-- 1.专门定义一个结果映射,在这个结果映射当中指定数据库表的字段名和Java类的属性名的对应关系。 2.type属性:用来指定POJO类的类名。 3. id属性:指定resultMap的唯一标识。这个id将来要在select标签中使用。 --> <resultMap id="student" type="shh.pojo.Student"> <!--如果这个表中有主键的话,建议使用id标签来指定主键的映射关系,可以提高执行效率。--> <id column="id" property="id"></id> <!--property后面填写POJO类的属性名--> <!--column后面填写数据库表的字段名--> <result column="name" property="n"></result> <result column="age" property="a"></result> <result column="gender" property="g"></result> <result column="birthday" property="b"></result> </resultMap> <!--使用resultMap属性来指定使用哪一个属性名映射,将属性值设置为已经定义了的某个resultMap的id值--> <select id="queryAll" resultMap="student"> select * from students; </select> </mapper>
-
-
-
是否开启驼峰命名自动映射
-
使用这种方式的前提是:属性名遵循Java的命名规范(小驼峰),数据库表的列名遵循SQL的命名规范。
-
Java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式。
-
SQL命名规范:全部小写,单词之间采用下划线分割。
-
比如以下的对应关系:
![]()
-
-
在mybatis-config.xml也就是mybatis的核心配置文件的settings标签中配置:
<!--放在properties标签后面--> <settings> <setting name="mapUnderscoreToCamelCase"value="true"/> </settings>
-
-
返回总记录条数:也就是返回一个基本数据类型:没啥区别
十一、动态SQL(重要)
-
有些业务场景需要进行sql拼接,这就需要动态SQL。例如批量删除、多条件查询等等。
-
if标签
-
使用if标签的时候,需要注意:
-
if标签中test属性是必须的。
-
if标签中test属性的值是false或者true。
-
如果test是true,则if标签中的sql语句就会拼接。反之,则不会拼接。
-
在mybatis的动态SQL当中,不能使用&&,只能使用and。
-
-
test属性中可以使用的是:
-
当使用了@Param注解,那么test中要出现的是@Param注解指定的参数名。@Param("brand"),那么这里只能使用brand
-
当没有使用@Param注解,那么test中要出现的是param1 param2 param3 arg0 arg1 arg2....
-
当使用了POJO,那么test中出现的是POJO类的属性名。
-
-
场景示例:多条件查询,可能的条件包括品牌、指导价格、汽车类型。
-
定义dao接口:
public interface CarDao { List<Car> select(@Param("brand") String brand, @Param("guidePrice") Integer guidePrice, @Param("carType") String carType); } -
编写XxxMapper.xml文件
<mapper namespace="shh.dao.CarDao"> <select id="select" resultType="shh.pojo.Car"> select * from t_car where 1 = 1 <if test="brand != null and brand != ''"> and brand like "%"#{brand}"%" </if> <if test="guidePrice != null and guidePrice !=''"> and #{guidePrice} > guide_price </if> <if test="carType != null and carType !=''"> and car_type = #{carType} </if> </select> </mapper> -
注意:添加了where 1=1标签,是为了满足sql格式,否则会产生sql语法错误。
-
-
-
where标签
-
where标签的作用:让where子句更加动态智能。所有条件都为空时,where标签保证不会生成where子句,并且可以自动去除某些条件前面多余的and或or(后面的and 或者 or不能去掉)。
-
使用示例:
-
dao接口:沿用if例子中的dao接口即可
-
XML文件:
<mapper namespace="shh.dao.CarDao"> <select id="select" resultType="shh.pojo.Car"> select * from t_car <where> <if test="brand != null and brand != ''"> and brand like "%"#{brand}"%" </if> <if test="guidePrice != null and guidePrice !=''"> and #{guidePrice} > guide_price </if> <if test="carType != null and carType !=''"> and car_type = #{carType} </if> </where> </select> </mapper> -
添加where标签,可以省略掉where 1=1 的条件判断,代码更加优雅了。
-
-
-
trim标签:用于添加、删除前后缀
-
trim标签的属性:
-
prefix:加前缀
-
suffix:加后缀
-
prefix0verrides:删除前缀
-
suffixOverrides:删除后缀
-
-
使用示例:
-
dao层接口:沿用上面的dao接口
-
Mapper.xml文件:
<mapper namespace="shh.dao.CarDao"> <select id="select" resultType="shh.pojo.Car"> select * from t_car <!-- prefix="where"是在trim标签中所有内容的前面添加where,只有在trim标签中的内容生效了,才会添加where--> <!--suffix0verrides="and|or"把trim标签中内容的多余后缀and或or去掉--> <trim prefix="where" suffixOverrides="and|or"> <if test="brand != null and brand != ''"> brand like "%"#{brand}"%" and </if> <if test="guidePrice != null and guidePrice !=''"> #{guidePrice} > guide_price and </if> <if test="carType != null and carType !=''"> car_type = #{carType} </if> </trim> </select> </mapper>
-
-
-
set标签
-
set标签:主要使用在update语句当中,用来生成set关键字,同时去掉最后多余的“,”,比如我们只更新提交的不为空的字段,如果提交的数据是空或者"",那么这个字段我们将不更新。
-
示例:
-
dao接口
public interface CarDao { int update(Car car); } -
XML文件
<mapper namespace="shh.dao.CarDao"> <update id="update" parameterType="car"> update t_car <set> <if test="carNum != null and carNum != ''">car_num = #{carNum},</if> <if test="brand != null and brand != ''">brand = #{brand},</if> <if test="guidePrice != null and guidePrice != ''">guide_price = #{guidePrice},</if> <if test="produceTime != null and produceTime != ''">produce_time = #{produceTime},</if> <if test="carType != null and carType != ''">car_type = #{carType}</if> </set> where id = #{id}; </update> </mapper>
-
-
-
choose、when、otherwise
-
这三个标签一般是一起使用的:类似if..else if...else结构。这里面,只能生效一个when或者otherwise。
<choose> <when></when> <when></when> <when></when> <otherwise></otherwise> </choose> -
示例:先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据生产日期查询。
-
dao接口
public interface CarDao { List<Car> select(@Param("brand") String brand, @Param("guidePrice") Integer guidePrice, @Param("carType") String carType); } -
XML文件
<select id="select" resultType="shh.pojo.Car"> select * from t_car <where> <choose> <when test="brand != null and brand != ''"> brand like "%"#{brand}"%" and </when> <when test="guidePrice != null and guidePrice !=''"> #{guidePrice} > guide_price and </when> <otherwise> car_type = #{carType} </otherwise> </choose> </where> </select>
-
-
-
foreach标签
-
作用:循环数组或集合,动态生成sql。
-
foreach标签的属性:
-
collection:指定数组或者集合。这个数组或者集合的参数名依然需要满足前面提到的,存储到内置map集合中的key值的规则。
-
item:代表数组或集合中的元素
-
separator:循环之间的分隔符
-
open:for循环外面以什么开始
-
close:for循环外面以什么结束。
-
-
示例:批量删除数据。
-
dao接口
public interface CarDao { int delete(@Param("ids") long ids[]); } -
XML文件
<delete id="delete" > delete from t_car where id in ( <foreach collection="ids" item="id" separator=","> #{id} </foreach> ) </delete>
-
-
open和close属性示例:下方写法与上方写法效果是同等的。
<delete id="delete" > delete from t_car where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete> -
批量插入数据示例:
-
dao接口
public interface CarDao { int insert(@Param("cars") List<Car> cars); } -
XML文件
<insert id="insert"> insert into t_car values <foreach collection="cars" item="car" separator=","> (null,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType}) </foreach> </insert>
-
-
批量删除数据示例:
-
dao接口
public interface CarDao { int delete(@Param("ids") Long ids[]); } -
XML文件
<delete id="delete" > delete from t_car where <foreach collection="ids" item="id" separator="or"> id = #{id} </foreach> </delete>
-
-
-
sql标签和include标签
-
sql标签用来声明sql片段;include标签用来将声明的sql片段包含到某个sq|语句当中
-
作用:代码复用、易维护。
-
使用示例:
<!-- 声明一个sql片段。 --> <sql id="carColumnNameSql"> id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType </sql> <select id="selectById2" resultType="car"> select <!--refid指定sql标签的id --> <include refid="carColumnNameSql"/> from t_car where id = #{id} </select>
-
十二、MyBatis的高级映射及延迟加载
-
高级映射:其实就是多表操作。多张表数据映射到一个java对象。这里高级映射主要讲了两个方面,一对多映射和多对一映射。
-
怎么分主表和副表?谁在前谁是主表。
-
多对一:多在前,那么多就是主表。
-
一对多:一在前,那么一就是主表。
-
高级映射,多张表将被映射到一个java对象中,那么映射到哪一个对象中呢?会映射到对应主表的对象中。
-
-
多对一映射
-
多对一映射,那么多的一方为主表。例如学生表和班级表,多个学生对应一个班级,也就是多对一的关系,那么学生表是多的一方,学生表就是主表,班级表就是副表。
![]()
-
如果进行映射,将多张表映射到一个java对象中?有多种方式,常见的包括三种:
-
第一种方式:一条SQL语句,级联属性映射
-
第二种方式:一条SQL语句,association标签。
-
第三种方式:两条SQL语句,分步查询。(这种方式常用:优点一是可复用。优点二是支持懒加载。)
-
-
-
多对一映射 ---- 级联属性映射
-
级联属性映射:一条SQL 语句,级联属性映射。级联属性映射,就是利用resultMap标签对属性和字段进行映射,内部对象的所属属性也进行映射,而SQL语句就进行表的连接进行查询。
-
示例:有两张表sutdent表和class表,student表和class表是多对一的关系。通过id查找student信息,包括班级信息
-
student类:在sudent类,也就是多的一方,添加一个属性指向一的一方。这里是Class clazz属性。
public class Student { private Long sid; private String sName; private Class clazz; } -
class类
public class Class { private Long cid; private String cname; } -
dao接口
public interface StudentDao { public Student selectById(@Param("id") Long id); } -
XML文件
<resultMap id="student_Class" type="shh.pojo.Student"> <result column="sid" property="sid"/> <result column="sname" property="sName"/> <result column="cid" property="clazz.cid"/> <result column="cname" property="clazz.cname"/> </resultMap> <select id="selectById" resultMap="student_Class"> select s.*,c.* from students s left join class c on c.cid = s.cid where s.sid = #{id} </select>
-
-
-
多对一映射 ---- 一条sql语句,采用association标签来实现
-
association标签:翻译为关联。用于resultMap标签中,让一个java属性关联另一个类。
-
property:提供要映射的POJO类的属性名。
-
javaType:用来指定要映射的java类型。
-
-
示例:有两张表sutdent表和class表,student表和class表是多对一的关系。通过id查找student信息,包括班级信息
-
dao接口
public interface StudentDao { public Student selectById(@Param("id") Long id); } -
XML文件
<resultMap id="student_Class" type="shh.pojo.Student"> <result column="sid" property="sid"/> <result column="sname" property="sName"/> <!--将Student类中的clazz属性关联到Class类。--> <association property="clazz" javaType="Class"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> </association> </resultMap> <select id="selectById" resultMap="student_Class"> select s.*,c.* from students s left join class c on c.cid = s.cid where s.sid = #{id} </select>
-
-
-
多对一映射 ---- 两条sql语句,分步查询
-
这种方式常用:
-
复用性增强。可以重复利用。(大步拆成N多个小碎步。每一个小碎步更加可以重复利用。)
-
采用这种分步查询,可以充分利用他们的延迟加载/懒加载机制。
-
-
示例:有两张表sutdent表和class表,student表和class表是多对一的关系。通过id查找student信息,包括班级信息
-
注意:依然会使用到association标签,会用到两个新的属性:cloum、select
-
第一步:dao接口,查询出满足id=1l的student信息
public interface StudentDao { public Student selectStep1(@Param("sid") Long id); } -
第二步:XML文件
<resultMap id="student_Class" type="shh.pojo.Student"> <result column="sid" property="sid"/> <result column="sname" property="sName"/> <!--将Student类中的clazz属性关联到select指定的selectId的查询结果中。--> <!-- 这里由于查找class的时候,需要使用cid作为参数,所以必须指定column=cid, 就是表示将这个查询出来的属性值作为下一个查询的参数。 --> <association property="clazz" select="shh.dao.ClassDao.selectStep2" column="cid"/> </resultMap> <select id="selectStep1" resultMap="student_Class"> select sid,sname,cid from students where sid = #{sid} </select> -
第三步:通过查询出来的cid去查询班级信息,dao层:
public interface ClassDao { Class selectStep2(@Param("cid") Long cid); } -
第四步:XML文件
<mapper namespace="shh.dao.ClassDao"> <select id="selectStep2" resultType="shh.pojo.Class"> select * from class where cid = #{cid} </select> </mapper>
-
-
上述整个代码流程的示意图:
![]()
-
什么是延迟加载(懒加载),有什么用?延迟加载的核心原理是:用的时候再执行查询语句。不用的时候不查询。可以提高性能。
-
-
延迟加载
-
注意:默认情况下是没有开启延迟加载的。
-
在mybatis当中怎么开启延迟加载呢?
-
一种方式是在association标签中添加
fetchType="lazy"。这样的话只有association标签中涉及到的select属性指向的地方会进行懒加载,也就是局部懒加载。 -
开启全局的懒加载:所有带有分步的,都采用延迟加载
<settings> <!--延迟加载的全局开关。默认值false不开启。--> <!--什么意思:所有带有分步的,都采用延迟加载。--> <setting name="lazyLoadingEnabled" value="true"/> </settings>
-
-
当开启全局懒加载的时候,也可以让某一条sql不懒加载,同样是利用association标签中的fetchType属性,设置其值为eager即可。实际开发中,一般会打开全局懒加载,然后哪个地方不需要使用懒加载的话,再具体设置即可。
-
体会懒加载:
-
在上面的分布查询的例子中,没有开启懒加载,默认是不开启的。那么执行以下测试代码:
public static void main(String[] args) throws ParseException { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); StudentDao mapper = sqlSession.getMapper(StudentDao.class); // 只需要使用student表中的sname属性。 System.out.println(mapper.selectStep1(1l).getSName()); sqlSession.commit(); sqlSession.close(); } -
执行结果分析:只需要使用student表中的sname属性,但是这个时候可以看到依然需要执行两条sql,也就是分步查询中的两个步骤都需要走一遍。但是其实第二步是不需要的,因为只需要student表的sname属性。
![]()
-
开启懒加载
<mapper namespace="shh.dao.StudentDao"> <resultMap id="student_Class" type="shh.pojo.Student"> <result column="sid" property="sid"/> <result column="sname" property="sName"/> <!--将Student类中的clazz属性关联到select指定的selectId的查询结果中。--> <!--开启懒加载,将fetchType设置为lazy--> <association property="clazz" select="shh.dao.ClassDao.selectStep2" column="cid" fetchType="lazy"/> </resultMap> <select id="selectStep1" resultMap="student_Class"> select sid,sname,cid from students where sid = #{sid} </select> </mapper> -
再次执行测试代码,可以发现只执行了一条sql,第二步的sql不再执行,这就提高了效率。
![]()
-
-
-
一对多映射
-
一对多映射,一是主表,所以通常是在一的一方中有List集合属性。例如一班级表class对应多个学生student,在Clazz类中添加List
stus;属性。 -
一对多的实现通常包括两种实现方式:
-
第一种方式:collection
-
第二种方式:分步查询
-
-
-
一对多映射 --- collection
-
示例:有两张表sutdent表和class表,student表和class表是多对一的关系。通过id查找student信息,包括班级信息
-
student类
public class Student { private Long sid; private String sName; } -
Class类:一对多,在一的一方添加一个List集合类型的属性,用来保存多的一方的对象。这里是List
students属性。 public class Clasz { private Integer cId; private String cName; private List<Students> studentsList; } -
dao接口
public interface ClaszDao { Clasz selectClaszById(@Param("cid") Integer cid); } -
XML文件
<resultMap id="class_students" type="clasz"> <result column="cid" property="cId"/> <result column="cname" property="cName"/> <!--ofType属性用于指定集合中元素的类型--> <collection property="studentsList" ofType="students"> <id column="sid" property="sId"/> <result column="sname" property="sName"/> <result column="cid" property="cId"/> </collection> </resultMap> <select id="selectClaszById" resultMap="class_students"> select c.*,s.* from class c left join students s on c.cid = s.cid where c.cid = #{cid} </select>
-
-
-
一对多映射 —— 分步查询
-
需要使用collection标签:在resultMap中使用collection标签,类似association标签那样,属性:
-
ofType属性:用来指定集合当中的元素类型
-
property属性:主表中的属性。
-
-
示例:有两张表sutdent表和class表,student表和class表是多对一的关系。通过id查找student信息,包括班级信息
-
第一步:dao接口,查询出满足cid=1000l的class信息
public interface ClassDao { Class selectById(@Param("cid") Long cid); } -
第一步:XML文件
<resultMap id="class_students" type="class"> <id column="cid" property="cid"/> <result column="cname" property="cname"/> <collection property="students" select="shh.dao.StudentDao.selectByCid" column="cid"> <id column="sid" property="sid"/> <result column="sname" property="sName"/> </collection> </resultMap> <select id="selectById" resultMap="class_students"> select cid,cname from class where cid = #{cid}; </select> -
第二步:通过查询出来的cid去查询学生信息,dao层:
public interface StudentDao { List<Student> selectByCid(@Param("cid") Long Cid); } -
第二步:XML文件
<mapper namespace="shh.dao.StudentDao"> <select id="selectByCid" resultType="shh.pojo.Student"> select * from students where cid = #{cid} </select> </mapper>
-
-
-
此外还有一对一、多对多关系的映射,尝试实现。
十三、MyBatis的缓存
-
MyBatis的缓存机制:执行DQL(select语句)的时候,将查询结果放到缓存当中(内存当中),如果下一次还是执行完全相同的语句,直接从缓存中拿数据。不再查数据库了。不再去硬盘上找数据了。知道数据被修改了,mybatis才会清理缓存,重新去数据库中获取数据。在这样的机制下,可以使用减少IO的频率,减少了项目进行硬盘操作的频率,从而提高效率。
-
mybatis缓存包括:缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
-
一级缓存:将查询到的数据存储到Sqlsession中。(注意考虑SqlSession和SqlSessionFactory的生存周期)
-
二级缓存:将查询到的数据存储到SqlSessionFactory中。
-
或者集成其它第三方的缓存:比如EhCache【Java语言开发的]、Memcache[C语言开发的]等。
-
-
一级缓存:缓存在SqlSession中。
-
一级缓存默认是开启的。不需要做任何配置。只要使用同一个SqlSession对象执行同一条SQL语句,就会走缓存。
-
思考:什么时候不走缓存?SqlSession对象不是同一个,肯定不走缓存; 查询条件不一样,肯定也不走缓存。
-
思考:什么时候一级缓存失效? 执行了sqlSession的clearCache()方法,这是手动清空缓存;执行了INSERT或DELETE或UPDATE语句。不管你是操作哪张表的,都会清空一级缓存,也就是只要SqlSession对象执行了修改数据的操作,就会清理SqlSession的缓存。
-
-
二级缓存:
-
二级缓存的范围是SqlSessionFactory
-
使用二级缓存需要具备以下几个条件:
-
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true,无需设置。
<setting name="cacheEnabled" value="true">: -
默认情况下,二级缓存机制是开启的。只需要在对应的SqlMapper.xml文件中添加以下标签。用来表示“我"使用该二级缓存即可。
<cache/> -
使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
-
SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。(李姐:如果SqlSession对象还在, 那么直接使用SqlSession对象的一级缓存即可,不需要使用到二级缓存)
-
-
二级缓存的失效:只要两次查询之间出现了增删改操作,二级缓存就会失效。【一级缓存也会失效]
-
-
二级缓存的相关配置:
![]()
-
eviction:指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
-
LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
-
FIFO: First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
-
SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
-
WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
-
-
flushlnterval:二级缓存的刷新时间间隔。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
-
readOnly:
-
true: 多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。
-
false: 多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
-
-
size:设置二级缓存中最多可存储的java对象数量。默认值1024。
-
-
MyBatis集成EhCache
-
集成EhCache是为了代替mybatis自带的二级缓存。一级缓存是无法替代的。
-
mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。
-
EhCache是Java写的。Memcache是C语言写的。所以mybatis集成EhCache较为常见,按照以下步骤操作,就可以完成集成
-
第一步:引入mybatis整合ehcache的依赖。
<!--mybatis集成ehcache的组件--> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.2</version> </dependency> -
在类的根路径下新建echcache.xml文件,并提供以下配置信息。
<?xml version="1.8" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <!--磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于Windows系统的虚拟内存--> <diskStore path="e:/ehcache"/> <!--defaultCache:默认的管理策略--> <!--eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断--> <!--maxElementsInMemory:在内存中缓存的element的最大数目--> <!--overflowToDisk:如果内存中数据超过内存限制,是否要缓存到磁盘上--> <!--diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false--> <!--timeToIdleSeconds:对象空闲时间(单位:秒),指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问--> <!--timeToLiveSeconds:对象存活时间(单位:秒),指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问--> <!--memoryStoreEvictionPolicy:缓存的3种清空策略--> <!--FIFO: first in first out (先进先出)--> <!--LFU: Less Frequently Used(最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit属性,hit值最小的将会被清出缓存--> <!--LRU: Least Recently Used(最近最少使用).(ehcache默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当 <defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/> --> </ehcache> -
第三步:修改SqlMapper.xml文件中的cache标签,添加type属性。
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
-
十四、MyBatis的逆向工程
-
所谓的逆向工程是:根据数据库表逆向生成Java的pojo类,SqlMapper.xml文件,以及Mapper接口类等。要完成这个工作,需要借助别人写好的逆向工程插件。
-
第一步:配置插件
<!-- mybatis逆向工程依赖 --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> </dependency> <!--配置mybatis逆向工程的插件--> <!--定制构建过程--> <build> <plugins> <!-- 自动生成mybatis-generator-core依赖引入(核心) --> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <!--允许覆盖--> <configuration> <overwrite>true</overwrite> </configuration> <dependencies> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> </dependency> </dependencies> </plugin> </plugins> </build>![]()
-
第二步:配置generatorConfig.xml。该文件名必须叫做:generatorConfig.xml,该文件必须放在类的根路径下。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime有两个值: MyBatis3Simple:生成的是基础版,只有基本的增删改查。 MyBatis3:生成的是增强版,除了基本的增删改查之外还有复杂的增删改查。 --> <context id="context1" target="MyBatis3Simple"> <!--防止生成重复代码--> <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/> <commentGenerator> <!--是否去掉生成日期--> <property name="suppressDate" value="true"/> <!--是否去除注释--> <property name="suppressAllComments" value="true"/> </commentGenerator> <!-- 数据库信息 --> <jdbcConnection connectionURL="jdbc:oracle:thin:@//127.0.0.1:1521/LOUTE" driverClass="oracle.jdbc.driver.OracleDriver" password="scott" userId="scott"> <!-- 针对oracle数据库 --> <property name="remarksReporting" value="true"></property> </jdbcConnection> <!-- 配置pojo生成的位置 --> <javaModelGenerator targetPackage="com.自己的.demo.pojo" targetProject="自己的/src/main/java"> <!--是否开启子包--> <property name="enableSubPackages" value="true" /> <!--是否去除字段名的前后空白--> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- 配置sql映射文件的生成位置 --> <sqlMapGenerator targetPackage="mapper" targetProject="自己的/src/main/resources"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </sqlMapGenerator> <!-- 生成Mapper接口的包名和位置--> <javaClientGenerator type="xMapper" targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 表名和对应的实体类名--> <table tableName="t_car1" domainObjectName="Car1"/> <table tableName="t_car2" domainObjectName="Car2"/> <table tableName="t_car3" domainObjectName="Car3"/> </context> </generatorConfiguration> -
执行插件即可。
十五、MyBatis使用PageHelper
-
PageHelper:用于实现分页的插件
-
第一步:引入依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.1</version> </dependency> -
第二步:在mybatis-config.xml文件中配置插件,在typeAliases标签下面进行配置:
<!--mybatis分页的拦截器--> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins> -
第三步:写代码使用这个组件了。
-
mapper.xml文件
<mapper namespace="com.powernode.mybatis.mapper.CarMapper"> <select id="selectall" resultType="Car"> select * from t_car </select> </mapper> -
基本使用:
@Test public void testSelectAll(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 一定一定一定要注意:在执行DQL语句之前。开启分页功能。 int pageNum = 2; int pageSize = 3; PageHelper.staritPage(pageNum, pageSize); List<Car> cars = mapper.selectAll(); cars.forEach(car -> System.out.println(car)); sqlSession.close(); } -
获取分页之后的分页数据:PageInfo<?> 对象封装如当前页、是否有下一页、是否有上一页、当前页数据量、上一页页面、是否是最后一页、是否是第一页等等贼多。
@Test public void testPageHelper() throws Exception{ SqlSessionFactorysqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")); SqlSession sqlSession =sqlSessionFactory.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); // 开启分页 PageHelper.startPage(2,2); // 执行查询语句 List<Car> cars = mapper.selectAll(); // 获取分页信息对象:5表示当前页数 PageInfo<Car> pageInfo = new PageInfo<>(cars, 5); System.out,println(pageInfo); }
-
十六、MyBatis的注解式开发
-
mybatis中也提供了注解式开发方式,采用注解可以减少Sql映射文件的配置,使用注解代替xml文件。当然,使用注解式开发的话,sql语句是写在java程序中的,这种方式也会给sql语句的维护带来成本。官方是这么说的:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java注解不仅力不从心,还会让你本就复杂的SQL语句更加混乱不堪。因此,如果你需要做一些很复杂的操作,最好用XML来映射语句。
-
需要注意:使用这个的时候,需要将核心配置文件中的mappers映射写为以下方式:
<mappers> <package name="shh.dao"/> </mappers> -
直接上使用示例:
-
环境的话就还是正常的mybatis的使用环境即可。需要mybatis核心配置文件、
-
示例:
public interface CarDao { @Delete("delete from t_car where id=#{id}") int delete(@Param("id") Long id); @Update("update t_car set guide_price = #{guidePrice} where id = #{id}") int update(@Param("car") Car car); @Select("select * from t_car where id = #{id}") @ResultType(Car.class) Car selectById(@Param("id") Long id); // 是用类似resultMap标签的注解 @Select("select * from t_car where id = #{id}") @Results({ @Result(column = "id",property = "id"), @Result(column = "id",property = "id"), @Result(column = "id",property = "id"), @Result(column = "id",property = "id") }) Car selectByIdOtherName(@Param("id") Long id); }
-
-
最佳实战:使用XML配置文件来实现繁琐的操作,使用注解来实现简单的crud操作。










浙公网安备 33010602011771号