注解退散!纯XML打造MyBatis持久层的终极形态

发布于:2025-08-02 ⋅ 阅读:(10) ⋅ 点赞:(0)

继上一篇文章讲解了如何使用注解来实现MyBatis的开发,本篇文章将讲解第二种方式——XML

一、XML文件配置

        在 resources/mapper 目录下配置 MyBatis XML 文件路径,并为所有数据表创建对应的 XML 映射文件

mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml

二、XML实现持久层代码 

2.1、添加Mapper接口

        我们先定义一个简单的接口,里面声明一个方法

@Mapper
public interface UserInfoMapperXML {
    List<UserInfo> selectAll();
}

2.2、实现UserInfoMapper.xml

2.2.1、配置标准模板

        在XML文件中添加 MyBatis 的标准配置模板

<?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="org.aokey.mybatisdemo.mapper.UserInfoMapperXML">

</mapper>

 2.2.2、MybatisX 插件

        当然,此功能并非IDEA自带的功能,而是需要插件才能生效,按照以下步骤:

2.3、单元测试

         点击 Generate statement 后发现xml文件中多了 <select> 标签

<?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="org.aokey.mybatisdemo.mapper.UserInfoMapperXML">

    <select id="selectAll" resultType="org.aokey.mybatisdemo.model.UserInfo">
        select * from user_info
    </select>

</mapper>

以下是对标签的详细说明:

  • mapper 标签必须指定 namespace 属性,其值为对应 mapper 接口的全限定名(完整包名+类名)

  • 查询标签:用于执行数据库查询操作,包含以下关键属性:

    • Id 与接口中定义的方法名称一致,表示对该方法的具体实现
    • resultType指定返回结果的数据类型,通常为定义的实体类

        然后在 <select> 标签之间写入目标SQL语句,返回接口处,如下自动生成测试类

@SpringBootTest
class UserInfoMapperXMLTest {
    @Autowired
    private UserInfoMapperXML userInfoMapperXML;
    @Test
    void selectAll() {
        userInfoMapperXML.selectAll().stream().forEach(x->System.out.println(x));
    }
}

        运行后观察到,成功查询到所有用户信息

 三、XML实现增删改查

3.1、增(insertr)

3.1.1、传参插入数据

         继续在接口处添加插入方法

Integer insertUserInfo(UserInfo userInfo);
    <insert id="insertUserInfo">
        insert into user_info (username, password, age, gender,phone) values
            (#{username},#{password},#{age},#{gender},#{phone})
    </insert>
    @Test
    void insertUserInfo() {
        UserInfo userInfo=new UserInfo();
        userInfo.setId(9);
        userInfo.setUsername("灰太狼");
        userInfo.setPassword("123456");
        userInfo.setAge(18);
        userInfo.setGender(1);
        userInfo.setPhone("19999999999");
        userInfoMapperXML.insertUserInfo(userInfo);
    }

        观察运行结果:显示成功插入!

 3.1.2、@Param注解

        使用@Param设置参数名称时,其用法与注解类似,当参数为对象时,传入的属性应采用 对象.属性 的形式

    <insert id="insertUser">
        insert into user_info (username, `password`, age, gender, phone) values
            (#{userInfo.username},#{userInfo.password},#{userInfo.age},#
                {userInfo.gender},#{userInfo.phone})
    </insert>
3..1.3、返回自增 Id

        保持接口定义不变,在 xml 中实现时设置 useGeneratedKeys 和 keyProperty 属性

    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
        insert into user_info (username, `password`, age, gender, phone) values
            (#{userInfo.username},#{userInfo.password},#{userInfo.age},#
                {userInfo.gender},#{userInfo.phone})
    </insert>

3.2、删(delete)

    Integer deleteUserInfo(Integer id);
    <delete id="deleteUserInfo">
        delete from user_info where id = 10
    </delete>

3.3、改(update)

    Integer updateUserInfo(UserInfo userInfo);
    <update id="updateUserInfo">
        update user_info set phone = #{phone} where id = #{id}
    </update>

3.4、查(select)

        同样地,采用XML格式进行查询也存在数据封装的问题,上述查询代码能够正常显示全部数据的原因是 yml 配置文件中配置了驼峰自动转换

        当然除此之外,还有注解和起别名的方法(参考上一篇),Aokey已经测试过使用 as 关键字起别名方法对于 xml 同样适用,那么我们来使用第三种方法解决数据封装问题:

    <resultMap id="BaseMap" type="org.aokey.mybatisdemo.model.UserInfo">
        <id column="id" property="id"></id>
        <result column="delete_flag" property="deleteFlag"></result>
        <result column="create_time" property="createTime"></result>
        <result column="update_time" property="updateTime"></result>
    </resultMap>
    <select id="selectAll" resultMap="BaseMap">
        select id, username,`password`, age, gender, phone, delete_flag,
               create_time, update_time from user_info
    </select>

        只需在要执行的SQL标签中添加 resultMap 的 ID 即可

四、# { } 和 $ { }

4.1、传递 Interger 类型参数

        我们在代码中大量使用了 # { } 进行变量插入赋值

    <select id="selectByAge" resultType="org.aokey.mybatisdemo.model.UserInfo">
        select id, username,`password`, age, gender, phone from user_info 
        where age = #{age}
    </select>
    @Test
    void selectByAge() {
        List<UserInfo> userInfos = userInfoMapperXML.selectByAge(20);
        userInfos.stream().forEach(x->System.out.println(x));
    }

        查看运行结果:

        输入的参数并未直接拼接在后面,而是使用 " ? " 作为占位符来传递 id 值。这种SQL语句被称为  " 预编译SQL "

        预编译SQL(Prepared Statement)是一种数据库操作技术,先将SQL语句模板发送到数据库服务器进行编译和优化,后续只需传递参数即可重复执行,无需重新解析SQL结构

        我们修改 # { } $ { } 后重新查看日志输出:

        上图可观察到:输入参数直接拼接到SQL语句后面了

4.2、传递 String 类型的参数

    <select id="selectByName" resultType="org.aokey.mybatisdemo.model.UserInfo">
        select id, username,`password`, age, gender, phone from user_info
        where username = #{username}
    </select>

        完成控制台日志输出:
        我们修改 # { } $ { } 后重新查看日志输出:

    <select id="selectByName" resultType="org.aokey.mybatisdemo.model.UserInfo">
        select id, username,`password`, age, gender, phone from user_info
        where username = ${username}
    </select>

         报错 :where中的子句喜羊羊在未知列;仔细观察可见,此处喜羊羊直接拼接在SQL语句后面,但是传递参数是字符串,而喜羊羊并未加引号,我们手动添加引号后再次查找:

    <select id="selectByName" resultType="org.aokey.mybatisdemo.model.UserInfo">
        select id, username,`password`, age, gender, phone from user_info 
        where username = "${username}"
    </select>

        查询结果显示成功,"喜羊羊"参数作为字符串类型被直接拼接到SQL语句中

从上述两个示例可以看出:

        # { } 采用预编译 SQL 的方式,通过 ? 占位符提前对 SQL 进行编译,再将参数填充到语句中。它会根据参数类型自动添加引号 ''

        $ { } 会直接进行字符串替换,与 SQL 一起编译。如果参数是字符串类型,需要添加引号 ''

4.3、# { } 和 $ { } 的区别

        # { } $ { } 的区别就是 预编译SQL 和 即时SQL 的区别

4.3.1、性能

        通常情况下,同一条SQL语句往往会被重复执行,或者每次执行时仅参数值有所不同。如果每次都要完整经历语法解析、SQL优化和编译过程,执行效率就会明显降低

        预编译SQL会将编译后的语句缓存起来,当后续执行相同SQL时(仅参数不同),无需重复编译。这种方式避免了重复解析和优化过程,从而显著提高了执行效率

4.3.2、SQL注入问题

        SQL注入:通过篡改输入数据来修改预定义的SQL语句,从而实现恶意代码执行并攻击服务器的技术手段(由于未对用户输入进行有效验证,同时采用字符串拼接方式构造SQL语句,攻击者可在输入参数中嵌入SQL关键字,从而篡改SQL执行逻辑,实现恶意攻击目的 )

        正常访问:

    @Test
    void selectByName() {
        UserInfo userInfo=userInfoMapperXML.selectByName("喜羊羊");
        System.out.println(userInfo);
    }
    <select id="selectByName" resultType="org.aokey.mybatisdemo.model.UserInfo">
        select id, username,`password`, age, gender, phone from user_info
         where username = '${username}'
    </select>

        访问结果:

         SQL注入场景:

    @Test
    void selectByName() {
        List<UserInfo> userInfo=userInfoMapperXML.selectByName("' or 1 = '1");
        userInfo.stream().forEach(x->System.out.println(x));
    }

         访问结果:

        从查询结果可以看出,当前获取的数据与预期不符,因此,建议在查询字段中使用 # { } 预编译方式(若使用 $ { } 时参数加上单引号,或者加上参数校验,另外SQL注入问题并非使用 $ { } 就一定会出现,只是有可能)

4.4、排序功能

        日常生活中,我们在淘宝、京东或者拼多多等网站购买一颗给商品时,最注重的问题无非是价格和质量,我们搜索一件商品后, 通过会点击价格或者销量排序,此功能就是 $ { } 实现的应用场景

        我们对用户表进行排序:

        我们在代码中进行实现:

    <select id="selectByOrder" resultType="org.aokey.mybatisdemo.model.UserInfo">
        SELECT * FROM `user_info` order by age #{order}
    </select>
   //测试代码
    @Test
    void selectByOrder() {
        userInfoMapperXML.selectByOrder("desc").stream().forEach(x->System.out.println(x));
    }

   //接口处代码
    List<UserInfo> selectByOrder(String order);

        观察控制台输出日志:

         而在MySQL中排序时 DESC(降序) 和 ASC(升序)这两个关键词是不加单引号的,否则会报错,这时 $ { } 不加引号的机制就能应用在此场景了,观察查询结果:

    <select id="selectByOrder" resultType="org.aokey.mybatisdemo.model.UserInfo">
        SELECT * FROM `user_info` order by age ${order}
    </select>

         注意:除排序功能外,若把表名作为参数,也必须使用 $ { }

4.5、like 查询 

    //测试用例
    @Test
    void selectByLike() {
        userInfoMapperXML.selectByLike("18").stream().forEach(x->System.out.println(x));
    }

    //接口处
    List<UserInfo> selectByLike(String likes);
    <select id="selectByLike" resultType="org.aokey.mybatisdemo.model.UserInfo">
        SELECT * FROM `user_info` where phone like '#{likes}%'
    </select>

        观察控制台输出日志:

        更改为:$ { } 再次观察输出结果

        MySQL 的 CONCAT() 函数 用于将两个或多个字符串连接成一个字符串;支持任意数量的参数,若参数中包含 Null,则返回结果为 Null

        解决方法:利用 MySQL 内置的 CONCAT() 函数进行处理,具体实现代码如下:

    <select id="selectByLike" resultType="org.aokey.mybatisdemo.model.UserInfo">
        SELECT * FROM `user_info` where phone like concat('%',#{likes},'%')
    </select>


网站公告

今日签到

点亮在社区的每一天
去签到