文章目录
一、MyBatis的执行流程
- 首先要有MyBatis配置文件,包括MyBatis全局配置文件和MyBatis映射文件。
- MyBatis通过读取配置文件信息,构造出SqlSessionFactory,即会话工厂。
- 通过SqlSessionFactory会话工厂创建SqlSession会话,SqlSession的作用是操作数据库。
- SqlSession本身不能直接操作数据库,它是通过底层的Executor执行器来操作数据库的。
- Executor执行器要处理的SQL信息是封装到一个底层对象MappedStatement中。该对象包括:SQL语句、输入参数映射信息、输出结果集映射信息。其中输入参数和输出结果的映射类型包括Java的简单类型、HashMap集合对象、POJO对象类型。
二、自动映射和驼峰映射
MyBatis提供了自动映射功能,在默认情况下自动映射功能是开启的,使用它的好处在于能有效减少大量的映射配置,从而减少工作量。
在全局配置文件中的setting标签中有两个可以配置的选项,它们是控制自动映射和驼峰映射的开关:
- autoMappingBehavior
- mapUnderscoreToCamelCase
- 自动映射,autoMappingBehavior选项的取值范围是:
- NONE 不进行自动映射
- PARTIAL 默认值,只对没有嵌套结果集进行自动映射
- FULL 对所有结果集进行自动映射,包括嵌套结果集
默认情况下,就是PARTIAL,如果编写的SQL列名和POJO属性名保持一致,就会形成自动映射。
- 驼峰映射,mapUnderscoreToCamelCase
如果代码中都严格按照驼峰命名法(比如数据库字段名user_name,则POJO属性名为userName),那么只要在配置项中把mapUnderscoreToCamelCase设置为true即可。
三、#{…}和${…}的区别
MyBatis的Mapper.xml语句中parameterType向SQL语句传参有两种方式:#{…} 和 ${…}
#{…}表示一个占位符号,在预编译处理时,会把参数部分用一个占位符 ? 代替
${…}表示一个拼接符号,在动态解析过程中,简单的进行字符串替换,会导致SQL注入
举个栗子~
① SELECT * FROM tb_user WHERE user_name = #{username};
② SELECT * FROM tb_user WHERE user_name = ${username};
①预编译后,会变成SELECT * FROM tb_user WHERE user_name = ?;
②在动态解析的时候,会传入参数字符串,SELECT * FROM tb_user WHERE user_name = ‘duck’;
所以它们的区别就是,#{…} 这种取值是编译好SQL语句再取值,${…} 这种是取值以后再去编译SQL语句
- #{} 能够很大程度防止SQL注入, ${} 无法防止SQL注入
- $方式一般用于传入数据库对象,例如传入表名
- 一般能用#的就别用$
四、获取自增主键值和非自增主键值
我们在数据库表设计的时候,一般都会在表中设计一个自增的id作为表的主键。这个id也会关联到其它表的外键。这就要求往表中插入数据时能返回表的自增id,用这个ID去给关联表的字段赋值。有两种方法可以获取自增主键值:使用sql语句和添加属性
- 使用sql语句
<insert id="insertRole" parameterType="cn.entity.Role">
INSERT INTO t_role(role_name, role_desc) VALUES(#{roleName}, #{roleDesc})
<selectKey keyProperty="roleId" order="AFTER" resultType="int">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>
- SELECT LAST_INSERT_ID():得到刚刚insert进去记录的主键值,只适用于自增主键
- keyProperty:将得到的主键值设置到parameterType指定对象的属性中
- order:SELECT LAST_INSERT_ID()相对于insert语句的执行顺序
- 添加属性
<insert id="insertRole" parameterType="cn.entity.Role"
useGeneratedKeys="true" keyProperty="roleId">
INSERT INTO t_role(role_name, role_desc) VALUES(#{roleName}, #{roleDesc})
</insert>
- useGeneratedKeys:采用JDBC的Statement对象的getGeneratedKeys方法返回主键
- keyProperty:将得到的主键值设置到parameterType指定对象的属性中
那么非自增主键值应该怎么获取呢?使用Mysql的uuid()
<insert id="insertRole" parameterType="cn.entity.Role">
<selectKey keyProperty="roleId" order="BEFORE" resultType="String">
SELECT UUID()
</selectKey>
INSERT INTO t_role(role_id, role_name, role_desc) VALUES(#{roleId}, #{roleName}, #{roleDesc})
</insert>
- SELECT UUID():生成主键,将主键设置到对象的roleId属性中
- 在执行insert时,从对象中取出roleId属性值
五、传递多个参数的4种方式
现实的需求中常常需要传递多个参数,比如订单可以通过订单编号查询,也可以根据订单名称、日期或者价格等参数进行查询等等。假设通过role_name和role_desc对角色进行查询,这样就要传递两个参数了~
1. 顺序传参法
public List<Role> findRoleByOrder(String roleName, String roleDesc);
<select id="findRoleByOrder" resultType="Role">
SELECT * FROM tb_role
WHERE role_name=#{0} AND role_desc=#{1}
</select>
#{}里面的数字代表传入参数的顺序~
缺点:SQL语句表达不直观,且一旦参数顺序调整,容易出错
2. 使用Map传参
public List<Role> findRoleByMap(Map<String, Object> parameterMap);
此时传递给映射器的是一个map对象,使用它在SQL中设置对应的参数
<select id="findRoleByMap" parameterType="Map" resultType="Role">
SELECT * FROM tb_role
WHERE role_name=#{roleName} AND role_desc=#{roleDesc}
</select>
#{}里面的名称对应的是Map里面的key名称~
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
Map<String, Object> parameterMap = new HashMap<String, Object>();
parameterMap.put("roleName", "1");
parameterMap.put("roleDesc", "1");
Role role = roleMapper.findRoleByMap(parameterMap);
缺点:
- Map是一个键值对应的集合,使用者要通过阅读它的键,才能明了其作用
- 使用Map不能限定其传递的数据类型,因此业务性质不强,可读性差。
3. 使用注解@Param传参
public List<Role> findRoleByAnnotation(@Param("roleName") String roleName,
@Param("roleDesc") String roleDesc);
<select id="findRoleByAnnotation" resultType="Role">
SELECT * FROM tb_role
WHERE role_name=#{roleName} AND role_desc=#{roleDesc}
</select>
#{}里面的名称对应的是注解@Param括号里指定的名称~
此时,代码的可读性大大提高~推荐使用!
4. 通过JavaBean传参
先定义一个关于参数的POJO——RoleParams
package cn.entity.RoleParams;
public class RoleParams {
private String roleName;
private String roleDesc;
/** setter and getter **/
}
此时的接口方法:
public List<Role> findRoleByBean(RoleParams roleParams);
#{}里面的名称对应的是RoleParams类里面的成员属性~
<select id="findRoleByBean" parameterType="cn.entity.RoleParams" resultType="Role">
SELECT * FROM tb_role
WHERE role_name=#{roleName} AND role_desc=#{roleDesc}
</select>
引入JavaBean定义的属性作为参数,然后查询:
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
RoleParams roleParams = new RoleParams();
roleParams.setRoleName("1");
roleParams.setRoleDesc("1");
Role role = roleMapper.findRoleByBean(roleParams);
这种方法很直观,但缺点是需要建一个实体类,扩展不容易,需要加属性,看情况使用。
总结:
- 使用顺序传参和Map传参导致了业务可读性的丧失,导致后续扩展和维护的困难,果断废弃。
- 使用注解@Param传参,受到参数个数的影响。当参数个数<=5时,推荐使用。当参数个数大于5,将给调用带来困难,此时不推荐。
- 当参数个数大于5,建议使用JavaBean方式。
六、使用resultMap进行关联查询
MyBatis中使用resultMap完成高级输出结果映射。
什么是高级映射?高级映射就是多表关联映射:
- 将关联查询的列映射到一个pojo属性中(一对一)
- 将关联查询的列映射到一个List<pojo>中(一对多)
以项目中的实体类为例:
Product商品类里除了一些简单类型的成员变量,还有Shop实体类和ProductCategory实体类对象的成员变量,还有ProductImg的List集合成员变量。
public class Product {
private Long productId;
private String productName;
private String productDesc;
private String imgAddr;
private String normalPrice;
private String promotionPrice;
private Integer priority;
private Date createTime;
private Date lastEditTime;
private Integer enableStatus;
private List<ProductImg> productImgList;
private ProductCategory productCategory;
private Shop shop;
}
对应的tb_product表结构是这样的:
CREATE TABLE `tb_product` (
`product_id` int(100) NOT NULL AUTO_INCREMENT,
`product_name` varchar(100) NOT NULL,
`product_desc` varchar(2000) DEFAULT NULL,
`img_addr` varchar(2000) DEFAULT '',
`normal_price` varchar(100) DEFAULT NULL,
`promotion_price` varchar(100) DEFAULT NULL,
`priority` int(2) NOT NULL DEFAULT '0',
`create_time` datetime DEFAULT NULL,
`last_edit_time` datetime DEFAULT NULL,
`enable_status` int(2) NOT NULL DEFAULT '0',
`product_category_id` int(11) DEFAULT NULL,
`shop_id` int(20) NOT NULL DEFAULT '0',
PRIMARY KEY (`product_id`),
KEY `fk_product_procate` (`product_category_id`),
KEY `fk_product_shop` (`shop_id`),
CONSTRAINT `fk_product_procate` FOREIGN KEY (`product_category_id`) REFERENCES `tb_product_category` (`product_category_id`),
CONSTRAINT `fk_product_shop` FOREIGN KEY (`shop_id`) REFERENCES `tb_shop` (`shop_id`)
) ENGINE=InnoDB AUTO_INCREMENT=73 DEFAULT CHARSET=utf8
所以我们定义如下的resultMap:
<mapper namespace="com.yaya.o2o.dao.ProductDao">
<resultMap id="productMap" type="com.yaya.o2o.entity.Product">
<id column="product_id" property="productId"/>
<result column="product_name" property="productName" />
<result column="product_desc" property="productDesc" />
<result column="img_addr" property="imgAddr" />
<result column="normal_price" property="normalPrice" />
<result column="promotion_price" property="promotionPrice" />
<result column="priority" property="priority" />
<result column="create_time" property="createTime" />
<result column="last_edit_time" property="lastEditTime" />
<result column="enable_status" property="enableStatus" />
<association property="productCategory" column="product_category_id" javaType="com.yaya.o2o.entity.ProductCategory">
<id column="product_category_id" property="productCategoryId" />
<result column="product_category_name" property="productCategoryName"/>
</association>
<association property="shop" column="shop_id" javaType="com.yaya.o2o.entity.Shop">
<id column="shop_id" property="shopId"/>
<result column="owner_id" property="ownerId"/>
<result column="shop_name" property="shopName"/>
</association>
<collection property="productImgList" column="product_id" ofType="com.yaya.o2o.entity.ProductImg">
<id column="product_img_id" property="productImgId" />
<result column="detail_img" property="imgAddr" />
<result column="img_desc" property="imgDesc" />
<result column="priority" property="priority" />
<result column="create_time" property="createTime" />
<result column="product_id" property="productId" />
</collection>
</resultMap>
......
</mapper>
- resultMap元素
- id:对resultMap的唯一标识
- type:最终映射的java对象类型
- id元素——查询结果集中唯一标识的定义
- column:查询出来的列名
- property:type指定的 pojo中的属性名
- result元素——对普通列的映射定义
- column:查询出来的列名
- property:type指定的 pojo中的属性名
- association元素——用于映射关联查询单个对象的信息(“有一个”关系)
- property:映射的属性
- column:查询的列名
- javaType:映射的属性的实体类
- id,column,property同上
- collection元素——对关联查询到的多条记录映射到集合对象中(“有很多”关系)
- property:映射的属性
- column:查询的列名
- ofType:指定映射到list集合属性中pojo的类型
- id,column,property同上
然后我们在select中使用resultMap,通过商品id查询商品信息:
<mapper namespace="com.yaya.o2o.dao.ProductDao">
<select id="queryProductById" resultMap="productMap" parameterType="Long">
SELECT
p.product_id,
p.product_name,
p.product_desc,
p.img_addr,
p.normal_price,
p.promotion_price,
p.priority,
p.create_time,
p.last_edit_time,
p.enable_status,
p.product_category_id,
p.shop_id,
pm.product_img_id,
pm.img_addr AS detail_img,
pm.img_desc,
pm.priority,
pm.create_time
FROM tb_product p
LEFT JOIN tb_product_img pm
ON p.product_id=pm.product_id
WHERE p.product_id=#{productId}
ORDER BY pm.priority DESC
</select>
</mapper>
这样就能完成一对一、一对多的高级映射了~
resultType和resultMap实现一对一查询小结
- resultType:使用较为简单。如果pojo中没有包括查询出来的列名,需要增加列名对应的属性,即可完成映射。
- resultMap:需要单独定义resultMap,实现麻烦。如果有对查询结果有特殊的要求,使用resultMap可以完成将关联查询映射到pojo的属性中。
- resultMap可以实现延迟加载,resultType无法实现。
什么是延迟加载?
先从单表查询,需要时再从关联表去关联查询,大大提高数据库的性能,因为查询单表要比关联查询多张表速度要快。association、collection具备延迟加载的功能。
七、动态sql
MyBatis提供对SQL语句动态的组装能力,使用XML的几个简单的元素,便能完成动态SQL的功能。大量的判断都可以在MyBatis的映射XML里面配置,以达到许多需要大量代码才能实现的功能,大大减少了代码量,体现了MyBatis的灵活、高度可配置性和可维护性。
MyBatis的动态SQL包括以下几种元素:
- if 判断语句
- choose(when, otherwise) 相当于java中的switch和case
- trim(when, set) 辅助元素,用于处理特定的SQL拼装问题,比如去掉多余的and,or等
- foreach 循环语句
下面是项目中用到的if元素和foreach元素的代码实例~
1. if元素
场景:在更新商品信息的时候,只有当传入pojo里属性不为空的,才进行对列名的赋值和拼接
<update id="updateProduct" parameterType="com.yaya.o2o.entity.Product">
UPDATE tb_product
<set>
<if test="productName != null">product_name=#{productName},</if>
<if test="productDesc != null">product_desc=#{productDesc},</if>
<if test="imgAddr != null">img_addr=#{imgAddr},</if>
<if test="normalPrice != null">normal_price=#{normalPrice},</if>
<if test="promotionPrice != null">promotion_price=#{promotionPrice},</if>
<if test="priority != null">priority=#{priority},</if>
<if test="lastEditTime != null">last_edit_time=#{lastEditTime},</if>
<if test="enableStatus != null">enable_status=#{enableStatus},</if>
<if test="productCategory != null and productCategory.productCategoryId != null">
product_category_id=#{productCategory.productCategoryId}
</if>
</set>
WHERE product_id=#{productId}
AND shop_id=#{shop.shopId}
</update>
- test:在判断语句中,要与if联合使用
2. foreach元素
场景:批量添加商品图片
<insert id="batchInsertProductImg" parameterType="java.util.List">
INSERT INTO
tb_product_img(img_addr, img_desc, priority, create_time, product_id)
VALUES
<foreach collection="list" item="productImg" index="index" separator=",">
(
#{productImg.imgAddr},
#{productImg.imgDesc},
#{productImg.priority},
#{productImg.createTime},
#{productImg.productId}
)
</foreach>
</insert>
- collection:传递进来的类型,如数组、List、Set等集合
- item:循环中当前的元素
- index:当前元素在集合的位置下标
- separator:各个元素的间隔符
八、模糊查询
场景:对输入的店铺名称进行模糊查询
...
<if test="shopCondition.shopName != null">
and s.shop_name like '%${shopCondition.shopName}%'
</if>
...
- 使用${…}
s.shop_name LIKE '%${shopCondition.shopName}%'
可能会引起sql的注入,尽量避免使用${…}
- 使用#{…}
s.shop_name LIKE "%"#{shopCondition.shopName}"%"
因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ‘,所以这里 % 需要使用双引号" ",不能使用单引号’ ',否则查不到任何结果。
使用CONCAT()函数进行拼接:
s.shop_name LIKE CONCAT('%', '${shopCondition.shopName}', '%')
---------------------------------------------------------------
s.shop_name LIKE CONCAT('%', #{shopCondition.shopName}, '%')