MyBatis处理SQL语句映射

基础MyBatis问题以去看MyBatis基础

使用log4j设置日志在控制台打印SQL语句及其执行信息

也可以使用MyBatis基础中用的slf4j。

在pom.xml文件中引入log4j坐标依赖

xml 复制代码
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.12</version>
</dependency>

在resourecs目录下设置log4j.properties文件配置信息用于在控制台查看SQL语句的执行和拼接情况

properties 复制代码
log4j.rootLogger = INFO,CONSOLE,FILE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout

log4j.appender.CONSOLE.layout.ConversionPattern=%-4r %-5p [%t] %37c %3x - %m%n

log4j.appender.FILE=org.apache.log4j.DailyRollingFileAppender

log4j.appender.FILE.DatePattern='.'yyyy-MM-dd
log4j.appender.FILE.File=D:/logs/mybatis.log
log4j.appender.FILE.layout = org.apache.log4j.PatternLayout

log4j.appender.FILE.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [%c]-[%p]- %m%n

然后在MyBatis核心配置文件配置,如下:

MyBatis核心配置文件配置半自动映射和全自动映射

xml 复制代码
<!--mybatis全局配置-->
<settings>
    <!-- 如果使用log4j打印SQL语句,则需要配置 value 的值 -->
    <!-- value="STDOUT_LOGGING" 使用日志打印SQL语句(会在控制台自动打印执行的SQL语句)-->
    <setting name="logImpl" value="STDOUT_LOGGING"/>

    <!-- value="LOG4J" 不打印SQL语句,只打印日志信息-->
<!--        <setting name="logImpl" value="LOG4J"/>-->

    <!--
    默认情况下,MyBatis 会自动进行结果映射,即把数据库查询结果映射为 Java Bean。
    但是,如果数据库字段与 Java Bean 的属性名不一致,则需要设置自动映射为NONE,
    即不进行自动映射,此时需要手动进行映射。
    默认:半自动映射(封装),即自动把sql配置xml文件中select查询出来的数据封装成resultType或resultMap指定的JavaBean对象,
        如果封装了多个对象,则自动变成List集合返回。
    NONE:关闭自动映射(封装),关闭后,需要使用resultMap进行手动映射。
    FULL:完全自动映射(嵌套封装,一对一、一对多,对象套对象)
    -->
<!--        <setting name="autoMappingBehavior" value="NONE"/>-->
    <setting name="autoMappingBehavior" value="FULL"/>
</settings>

一、查

resultType:返回值类型,会自动把查出来的数据自动封装成该类型对象并返回,如果有多个,则会把封装的对象放到一个list集合中并返回。

自动封装时会按查询的数据库表的字段先后顺序让该数据库表的字段名与要封装成的对象的属性名进行匹配,若匹配上则会把该数据封装到对象中。(尤其是两张有相同字段名表联合查询,并且MyBatis进行自动封装时,会出现该字段的数据封装错误的问题,可见 1.6一对一 )。

1.1 #{} 和 ${}

dao层接口方法

java 复制代码
public Provider findProviderById(Integer id);

SQL映射

  • #{}是将参数数据以占位符的形式与SQL语句进行拼接,防止SQL注入攻击。
  • ${}是将参数数据以原样输出的形式与SQL语句进行拼接,但是不建议使用,容易出现SQL注入问题。
  • {}中的参数名要与接口方法中的参数名一致
  • sql语句最后的分号可不写。

(可以看日志在控制台输出执行的所拼接的sql语句,见Mybatis基础

xml 复制代码
<!--id要与接口中方法名一致-->
<!--parameterType:参数类型  可不写-->
<!--resultType:返回值类型,会把查出来的数据封装成该类型对象并返回,如果有多个,则会把封装的对象放到一个list集合中并返回-->
<!--sql语句最后的分号可不写-->
<select id="findProviderById" resultMap="ProviderMap">
    <!--
        #{}是将参数数据以占位符的形式与SQL语句进行拼接,防止SQL注入攻击
        ${}是将参数数据以原样输出的形式与SQL语句进行拼接,但是不建议使用,容易出现SQL注入问题
    -->
    select id,proCode,proName,proDesc from smbms_provider a where a.id=#{id}
</select>

1.2 特殊字符处理

特殊字符(比如>、<等特殊符号)处理,这里以查询id小于指定id的SQL语句为例:

  1. 转义字符:如<会被认为是一个标签的开始,需要转义为&lt;,一般用于少量使用特殊字符的SQL语句(>转义对应&gt;,>能正常使用,可以不用转义)。
  2. CDATA区(当作纯文本处理):写CD会有提示,如在里面写<号 <![CDATA[ < ]]>,一般用于大量使用特殊字符的SQL语句。
xml 复制代码
<select id="selectById" resultMap="brandResultMap">
	<!--使用转义-->
	select * from tb_brand where id &lt;= #{id};
	<!--使用CDATA区-->
    select * from tb_brand where id <![CDATA[ < ]]> #{id};
</select>

1.3 模糊查询不能用单引号

注意:这里模糊查询不能写单引号,如:'%${proName}%',这样写会报错。要用concat拼接,concat('%',#{pname},'%')。

xml 复制代码
<select id="findList" resultType="Provider">
    select id,proCode,proName,proDesc
    from smbms_provider a
    where proName like concat('%',#{pname},'%')
        and proDesc like concat('%',#{pdesc},'%')
</select>

1.4 自动封装数据问题

如果数据库表的字段名称(列名)和resultType指定的实体类的属性名称不一样,则不能自动封装数据(即若对应不上则封装后数据为null)。

有以下三个方案可以解决:

1.4.1 起别名

给与实体类中属性名不一样的列名起别名,这个别名要与对应的实体类中的属性名一致。

缺点:每次查询都要定义一次别名。

xml 复制代码
<select id="selectAll" resultType="brand">
	<!--brand_name是列名,要起别名与Brand类中属性brandName的属性名一致,否则无法封装-->
    select id, brand_name as brandName, company_name as companyName, ordered, description, status from tb_brand;
</select>

1.4.2 resultMap(最常用)

数据库字段与要封装成的实体类属性不一致才需要字段映射。

如果关闭了自动映射,则只能通过resultMap封装(映射)指定的数据

  1. 定义<resultMap>标签:
    • type:要封装成的数据类型
    • <id property="id" column="id"/>用来完成主键字段的映射。
    • <result property="brandName" column="brand_name"/>用来完成一般字段的映射,也能用于完成主键字段的映射(它俩只是为了区分字段,实际上没区别)。
      • property:实体类的属性名。
      • column:数据库表的字段名(列名)。
      • 若property和column的值一样,则可以不写这个字段映射。
      • jdbcType:数据库字段类型。
      • javaType:java字段类型。
      • jdbcType与javaType(可不写):告诉mybatis如何将数据库字段映射到java字段中,只是为了比较数据类型是否一致,一般都是写jdbcType="VARCHAR" javaType="String"。
  2. <select>标签中,使用resultMap属性替换resultType属性,并设置为对应resultMap的id。
xml 复制代码
<resultMap id="brandResultMap" type="brand">
    <!--数据库字段与实体类属性不一致才需要字段映射-->
    <!--id用来完成主键字段的映射,如下。若数据库字段名与实体类属性名一致,可以不用写-->
    <!--<id property="id" column="id"/>-->
    <!--result用来完成一般字段的映射-->
    <result property="brandName" column="brand_name" jdbcType="VARCHAR" javaType="String"/>
    <result property="companyName" column="company_name"/>
</resultMap>
<select id="selectById" resultMap="brandResultMap">
	select * from tb_brand where id=#{id};
</select>

1.4 动态SQL

1.4.1 where、if

动态SQL条件查询,如下案例

if:条件判断,满足条件才会拼接其中写的SQL。

test:判断条件(多个判断条件间要用英文 and 或 or 拼接,不能用&&和||)

问题:若第一个条件判断不成立,则该条件下的SQL不会拼接,导致后面拼接的SQL语句错误(原本的第二个条件变成了第一个条件,若该条件满足,则会以and开头,SQL语句错误)

解决:

  1. 恒等式(方法较笨):所有条件开头都加and,并且让 1=1 这个恒等式作为第一个条件。
  2. <where>标签(常用):
    • 替换where关键字,会自动把第一个条件中SQL开头的and去掉(如果and存在)。
    • 第一个条件中的SQL开头可不加and,后面的条件中的SQL必须以and开头。
    • 如果<where>标签中没有要拼接的条件内容,则执行SQL时不会添加where关键字。
xml 复制代码
<select id="selectByCondition" resultMap="brandResultMap">
        select *
        from tb_brand
<!--        where-->
				<!-- 若参数status为null,则该条件不会拼接,导致后面拼接的SQL语句错误(原本的第二个条件变成了第一个条件,且以and开头,SQL语句错误)-->
<!--            <if test="status!=null">-->
<!--                status = #{status}-->
<!--            </if>-->
<!--              -->
				<!--用恒等式-->
<!--            1 = 1-->
<!--            <if test="status!=null">-->
<!--                and status = #{status}-->
<!--            </if>-->
<!--            <if test="companyName!=null and companyName!=''">-->
<!--                and company_name like #{companyName}-->
<!--            </if>-->
<!--            <if test="brandName!=null and brandName!=''">-->
<!--                and brand_name like #{brandName}-->
<!--            </if>-->

        <where>
            <if test="status!=null">
                status = #{status}
            </if>
            <if test="companyName!=null and companyName!=''">
                and company_name like #{companyName}
            </if>
            <if test="brandName!=null and brandName!=''">
                and brand_name like #{brandName}
            </if>
        </where>
    </select>

1.4.2 choose、when、otherwise

相当于Java中的if、elseif。

xml 复制代码
<select id="selectByConditionSingle" resultMap="brandResultMap">
    select id, brand_name as brandName, company_name as companyName, ordered, description, status
    from tb_brand
<!--        where-->
<!--        <choose>&lt;!&ndash;最多只会拼接其中一个条件,谁先满足就拼接哪个条件&ndash;&gt;-->
<!--            <when test="status!=null">-->
<!--                status = #{status}-->
<!--            </when>-->
<!--            <when test="companyName!=null and companyName!=''">-->
<!--                company_name like #{companyName}-->
<!--            </when>-->
<!--            <when test="brandName!=null and brandName!=''">-->
<!--                brand_name like #{brandName}-->
<!--            </when>-->
<!--            <otherwise>&lt;!&ndash;前面都不满足时执行,避免没有传值时报错&ndash;&gt;-->
<!--                1 = 1-->
<!--            </otherwise>-->
<!--        </choose>-->

    <!--如果不想用<otherwise>,则可以用<where>把<choose>包裹住-->
    <where>
        <choose><!--最多只会拼接其中一个条件,谁先满足就拼接哪个条件-->
            <when test="status!=null">
                status = #{status}
            </when>
            <when test="companyName!=null and companyName!=''">
                company_name like #{companyName}
            </when>
            <when test="brandName!=null and brandName!=''">
                brand_name like #{brandName}
            </when>
        </choose>
    </where>
</select>

1.5 SQL片段

用于把一些重复使用的SQL语句的片段抽取出来,需要用时就调用。

使用<sql>标签定义SQL片段,id:唯一标识,调用时需要。

在SQL语句中使用<include>标签,通过refid指定要调用的SQL片段。

xml 复制代码
<!--sql片段-->
<sql id="brand_column">
    id, brand_name as brandName, company_name as companyName, ordered, description, status
</sql>
<select id="selectAll" resultType="brand">
	<!--没使用sql片段-->
    <!--select id, brand_name as brandName, company_name as companyName, ordered, description, status from tb_brand;-->
    
    <!--使用sql片段-->
	<!--使用include标签的refid属性调用sql片段-->
    select <include refid="brand_column"/> from tb_brand;
</select>

1.6 一对一(association、数据封装错误)

一个收获地址对应一个用户。

需要在Address类中添加一个User类的对象属性,用于存储用户信息,如:

java 复制代码
private User u;//存储用户对象信息

dao层接口方法

java 复制代码
public List<Address> findAddressAll();

SQL映射

xml 复制代码
<select id="findAddressAll" resultMap="AddressMap">
    select
    a.id,
    a.contact,
    a.addressDesc,
    a.tel,
    a.userId,
    b.id,
    b.userName
    from smbms_address a
    left join smbms_user b
    on a.userId=b.id
</select>
<resultMap id="AddressMap" type="Address">
<!--        jdbcType:数据库字段类型-->
<!--        javaType:java字段类型-->
<!--        jdbcType与javaType的作用:告诉mybatis如何将数据库字段映射到java字段中,只是为了比较数据类型是否一致,可不写,一般都是写jdbcType="VARCHAR" javaType="String"-->
    <id property="id" column="id"/>
    <result property="contact" column="contact" jdbcType="VARCHAR" javaType="String"/>
    <result property="addressDesc" column="addressDesc"/>
    <result property="tel" column="tel"/>
    <!-- 一对一 property:指定封装实体类对象的属性名 javaType:指定封装的java对象类型-->
    <!--如果想使用全自动映射,则必须有一个空的association,
    但与其写一个空的association不如用半自动映射,另写一个resultMap并引进来,进行手动封装-->
    <association property="u" javaType="User" resultMap="UserMap">
        <!--如果是半自动映射,若association里面不设置映射,则不会把这里面的数据封装-->
        <!--<id property="userName" column="userName"/>-->

        <!--
        自动封装时会按查询的数据库表的字段先后顺序让该数据库表的字段名与要封装成的对象的属性名进行匹配,
        因为先查的是smbms_address的表的id,且User类的id属性名与smbms_address表的id字段名一致,
        因此会先把address的id封装进user对象的id值,而不是封装后面查询的address_user的id,从而导致数据封装错误
        
        除非手动封装,如下,或者把user表的id字段改为user_id-->
<!--            <id property="id" column="userId"/>-->
    </association>
</resultMap>
<resultMap id="UserMap" type="User">
    <id property="id" column="userId"/>
    <id property="userName" column="userName"/>
</resultMap>

1.7 一对多(collection)

一个用户对应多个收获地址。

需要在User类中添加一个list集合属性,用于存储收获地址信息。如:

java 复制代码
private List<Address> addressList;//存储收获地址信息

dao层接口方法

java 复制代码
public User findUserById(Integer id);

SQL映射

xml 复制代码
<select id="findUserById" resultMap="UserMap">
    select a.id,
           a.userName,
           b.id as addrId,
           b.contact,
           b.addressDesc
    from smbms_user a
    left join smbms_address b
    on a.id = b.userId
    where a.id = #{id};
</select>
<!--id:唯一标识 type:映射的类型,支持别名-->
<resultMap id="UserMap" type="User">
    <!--若property和column的值一样,则可以不写这个字段映射。-->
    <!--<id property="id" column="id"/>-->
    <!--<result property="userName" column="userName"/>-->
    
    <!--一对多 addressList:(User类)属性集合的名称  ofType:要封装成的对象(Address类)-->
    <!--理解:就是把查询出来的数据中列名(或别名)与Address类的属性名对应的每一行数据封装成一个Address对象放到User类的addressList集合属性中-->
    <collection property="addressList" ofType="Address">
        <id property="id" column="addrId"/>
        <result property="contact" column="contact"/>
        <result property="addressDesc" column="addressDesc"/>
    </collection>
</resultMap>

1.8 foreach

自动遍历传过来的集合、数组、Map的key所对应的集合或数组

<foreach>标签:

item:获取每次循环的对象值

collection:指定循环集合list、array、map的key

index作用:获取集合元素索引,可充当参数使用,如:#{index}

  • 迭代List集合或数组时:index表示当前元素在List集合或数组中的下标位置(从 0 开始)
  • 迭代Map集合时:index表示当前元素的键(key)

open:前缀

close:后缀

separator:指定集合元素之间的分隔符

准备的sql片段

xml 复制代码
<sql id="userCols">
	id,userCode,userName,birthday
</sql>

1.8.1 传入集合

dao层接口方法

java 复制代码
public List<User> findUserList1(List<Integer> ids);

SQL映射

xml 复制代码
<select id="findUserList1" resultType="User">
    select
    <include refid="userCols"/>
    from smbms_user
    where id in
    <!--(1,2,3,4,5)-->
    <foreach item="id" collection="list" index="index" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

1.8.2 传入数组

dao层接口方法

java 复制代码
public List<User> findUserList2(Integer[] ids);

SQL映射

xml 复制代码
<select id="findUserList2" resultType="User">
    select
    <include refid="userCols"/>
    from smbms_user
    where id in
    <foreach item="id" collection="array" index="index" open="(" close=")" separator=",">
        #{id}
    </foreach>
</select>

1.8.3 传入 map

dao层接口方法

java 复制代码
public List<User> findUserList3(Map<String,Object> map);

SQL映射

xml 复制代码
<select id="findUserList3" resultType="User">
    select
    <include refid="userCols"/>
    from smbms_user
    where id in
    <!--这里ids是map的一个键,其值是一个数组-->
    <foreach item="id" collection="ids" index="index" open="(" close=")" separator=",">
        #{id}
    </foreach>
    and userName = #{name}
</select>

二、增、删、改

增、删、改默认返回int,无法修改,不用写resultType,写了会报错。

2.1 增(使用trim实现动态SQL)

dao层接口方法

java 复制代码
public int saveUser(User user);

SQL映射

<trim>标签:

  • prefix:给sql语句拼接的前缀。
  • suffix:给sql语句拼接的后缀。
  • prefixOverrides:自动去除sql语句前面的关键字或者字符(如果存在),该关键字或者字符由prefixOverrides属性指定,假设该属性指定为"AND",当sql语句的开头为"AND",trim标签将会去除该"AND"。
  • suffixOverrides:自动去除sql语句前面的关键字或者字符(如果存在),该关键字或者字符由prefixOverrides属性指定。
xml 复制代码
<insert id="saveUser">
    <!--
    为了实现动态SQL,可以使用trim标签
    -->
    insert into smbms_user
    <trim prefix="(" suffixOverrides="," suffix=")">
        <if test="userCode!=null">userCode,</if>
        <if test="userName!=null">userName,</if>
        <if test="userPassword!=null">userPassword,</if>
        <if test="gender!=null">gender,</if>
        <if test="birthday!=null">birthday,</if>
    </trim>
    values
    <trim prefix="(" suffixOverrides="," suffix=")">
        <if test="userCode!=null">#{userCode},</if>
        <if test="userName!=null">#{userName},</if>
        <if test="userPassword!=null">#{userPassword},</if>
        <if test="gender!=null">#{gender},</if>
        <if test="birthday!=null">#{birthday},</if>
    </trim>
</insert>

2.2 改(set标签)

dao层接口方法

java 复制代码
public int updateUser(User user);

SQL映射

直接使用set关键字会导致最后拼接SQL时要修改的内容最后多一个逗号,这就需要用到<set>标签,会自动去除拼接的SQL最后多余的逗号。
<set>标签只在修改里面用。

xml 复制代码
<update id="updateUser">
    update smbms_user
    <!--
        直接使用set会导致最后拼接SQL时要修改的内容最后多一个逗号,
        这就需要用到<set>标签,会自动去除拼接的SQL最后多余的逗号
        <set>标签只在修改里面用
    -->
    <set>
        <if test="userCode!=null">
            userCode=#{userCode},
        </if>
        <if test="userName!=null">
            userName=#{userName},
        </if>
        <if test="userPassword!=null">
            userPassword=#{userPassword},
        </if>
        <if test="gender!=null">
            userPassword= #{gender},
        </if>
        <if test="birthday!=null">
            birthday= #{birthday},
        </if>
    </set>
    <where>
        id=#{id}
    </where>
</update>

2.3 删

执行刪除SQL的映射没什么要注意的,这里就省略了。

三、接口方法传递参数

3.1 单个参数

一个参数,一般是不会用@Param注解的。

dao层接口方法

java 复制代码
public Provider findProviderById(Integer id);

SQL映射

xml 复制代码
<select id="findProviderById" resultMap="ProviderMap">
	select id,proCode,proName,proDesc from smbms_provider a where a.id=#{id}
</select>

3.2 多个参数

多个参数(两个及以上 ):必须使用注解 ,相当于给参数起别名,否则SQL映射文件不认。

注解:@Param(value="proName"),value=可省略,给参数指定别名,并在SQL配置xml文件中使用别名#{proName},#{proDesc}

dao层接口方法

java 复制代码
public List<Provider> findList(@Param("pname") String proName,@Param(value="pdesc") String proDesc);

SQL映射

xml 复制代码
<select id="findList" resultType="Provider">
    select id,proCode,proName,proDesc
    from smbms_provider a
    where proName like concat('%',#{pname},'%')
        and proDesc like concat('%',#{pdesc},'%')
</select>

3.3 对象参数

  1. 如果传的是一个实体类对象,则sql中可直接使用这个实体类对象的属性名。
  2. 如果传的实体类对象使用@Param注解起别名,则sql中必须使用#{别名.参数名}。

dao层接口方法

java 复制代码
//这里一个参数,正常是不会用@Param注解的,只是为了演示在使用注解的前提下,在sql配置xml文件中必须使用#{别名.属性名}进行接收传递的参数
public int addProvider(@Param(value="pro") Provider provider);

SQL映射

xml 复制代码
<insert id="addProvider">
	insert into smbms_provider(proCode,proName,proDesc) values(#{pro.proCode},#{pro.proName},#{pro.proDesc})
</insert>

3.4 使用Map集合能存所有参数

dao层接口方法

java 复制代码
public List<Provider> findListByMap(Map<String,Object> maps);

SQL映射

使用map的key作为参数进行传递

xml 复制代码
<select id="findListByMap" resultType="Provider">
    <!--使用map的key作为参数进行传递-->
    select id,proCode,proName,proDesc
    from smbms_provider a
    where proName like concat('%',#{name},'%')
      and proDesc like concat('%',#{desc},'%')
</select>