MyBatis Mapper 接口(Mapper Interface)编程与代理模式

MyBatis Mapper 接口编程与代理模式:一场面向接口的灵魂之旅

各位观众,欢迎来到“程序员脱口秀”!今天咱们要聊聊 MyBatis 里一个神奇的玩意儿:Mapper 接口。别听到“接口”俩字就害怕,今天咱保证把这玩意儿讲得生动有趣,让各位感觉就像在品一杯香浓的咖啡,回味无穷。

一、开场白:告别 XML 的时代?

话说当年,MyBatis 还叫 iBATIS 的时候,那配置文件,那是真叫一个庞大啊!每个 SQL 语句都得在 XML 里老老实实地写一遍,稍微改动一下,整个文件都得重新审视一遍,简直让人崩溃。那时候,我们程序员的日常就是:

  • 打开 XML
  • 找到对应的 SQL
  • 修改 SQL
  • 保存 XML
  • 重启应用
  • 祈祷不会出错

简直就是一场噩梦循环!

后来,MyBatis 痛定思痛,决定引入 Mapper 接口这种新玩意儿。它试图告诉我们:嘿,哥们,别再跟 XML 死磕了,试试面向接口编程吧!

二、什么是 Mapper 接口?

简单来说,Mapper 接口就是一个 Java 接口,里面定义了一些方法,每个方法都对应着一个 SQL 语句。但是,你不需要写这些方法的实现!MyBatis 会在运行时,根据你接口中的方法,自动生成对应的 SQL 执行代码。

是不是有点玄乎? 别着急,咱们一步一步来。

2.1 定义 Mapper 接口

首先,你需要创建一个接口,比如:

package com.example.mapper;

import com.example.entity.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface UserMapper {

    @Select("SELECT * FROM user WHERE id = #{id}")
    User getUserById(@Param("id") Long id);

    @Insert("INSERT INTO user (name, age) VALUES (#{name}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertUser(User user);

    @Update("UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}")
    int updateUser(User user);

    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteUser(@Param("id") Long id);

    @Select("SELECT * FROM user")
    List<User> getAllUsers();

}

这个接口定义了五个方法,分别对应着查询、插入、更新和删除 user 表的操作。注意看上面的 @Select, @Insert, @Update, @Delete 注解,它们告诉 MyBatis 哪个方法对应哪个 SQL 语句。 @Param 注解指定了参数的名字。 @Options 注解指定了使用主键自增,并且把主键id设置到user对象中。

2.2 配置 MyBatis

接下来,你需要告诉 MyBatis 去哪里找到这些 Mapper 接口。这可以通过在 MyBatis 的配置文件(通常是 mybatis-config.xml)中配置 Mapper 接口来实现:

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false&amp;serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="password"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

<mappers> 标签告诉 MyBatis 去 com.example.mapper 包下扫描所有的 Mapper 接口。

2.3 使用 Mapper 接口

最后,你就可以在你的代码中使用这些 Mapper 接口了:

package com.example.service;

import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class UserService {

    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

            // 查询用户
            User user = userMapper.getUserById(1L);
            System.out.println("User: " + user);

            // 插入用户
            User newUser = new User();
            newUser.setName("张三");
            newUser.setAge(25);
            int insertResult = userMapper.insertUser(newUser);
            System.out.println("Insert Result: " + insertResult + ", New User ID: " + newUser.getId());

            // 更新用户
            user.setName("李四");
            int updateResult = userMapper.updateUser(user);
            System.out.println("Update Result: " + updateResult);

            // 删除用户
            int deleteResult = userMapper.deleteUser(2L);
            System.out.println("Delete Result: " + deleteResult);

            // 查询所有用户
            List<User> allUsers = userMapper.getAllUsers();
            System.out.println("All Users: " + allUsers);

            sqlSession.commit(); // 提交事务
        }
    }
}

在这个例子中,我们通过 sqlSession.getMapper(UserMapper.class) 获取了 UserMapper 接口的实例。然后,我们就可以像调用普通方法一样,调用 UserMapper 接口中的方法了。

三、代理模式:幕后英雄

你可能会好奇,为什么我们只需要定义接口,而不需要写实现类,MyBatis 就能帮我们执行 SQL 语句呢? 这背后的功臣就是代理模式

3.1 什么是代理模式?

代理模式是一种设计模式,它允许你创建一个代理对象,来控制对另一个对象的访问。 代理对象就像一个中间人,它可以帮你做一些额外的事情,比如:

  • 权限控制:只有满足条件的用户才能访问目标对象。
  • 日志记录:记录每次对目标对象的访问。
  • 性能优化:缓存目标对象的结果,避免重复计算。

3.2 MyBatis 中的代理模式

在 MyBatis 中,当我们调用 sqlSession.getMapper(UserMapper.class) 时,MyBatis 实际上并没有返回一个 UserMapper 接口的真实实现类,而是返回了一个代理对象。 这个代理对象会拦截我们对 UserMapper 接口中方法的调用,然后将这些调用转换成 SQL 语句,并执行它们。

具体来说,MyBatis 使用了 Java 的动态代理机制来实现这个代理对象。 动态代理允许我们在运行时创建代理对象,而不需要提前编写代理类的代码。

3.3 动态代理的原理

Java 的动态代理主要依赖于 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口。

  • Proxy 类提供了一个静态方法 newProxyInstance(),可以用来创建一个代理对象。
  • InvocationHandler 接口定义了一个 invoke() 方法,当代理对象的方法被调用时,invoke() 方法会被执行。

在 MyBatis 中,InvocationHandler 的实现类负责将 Mapper 接口的方法调用转换成 SQL 语句,并执行它们。

3.4 代码示例

为了更好地理解 MyBatis 中的代理模式,我们可以自己写一个简单的例子:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface HelloService {
    String sayHello(String name);
}

class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello, " + name;
    }
}

class MyInvocationHandler implements InvocationHandler {

    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After method: " + method.getName());
        return result;
    }
}

public class ProxyExample {

    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        MyInvocationHandler handler = new MyInvocationHandler(helloService);

        HelloService proxy = (HelloService) Proxy.newProxyInstance(
                HelloService.class.getClassLoader(),
                new Class<?>[]{HelloService.class},
                handler
        );

        String message = proxy.sayHello("World");
        System.out.println(message);
    }
}

在这个例子中,MyInvocationHandler 类实现了 InvocationHandler 接口,它会在 sayHello() 方法执行前后打印一些信息。 通过 Proxy.newProxyInstance() 方法,我们创建了一个 HelloService 接口的代理对象。 当我们调用 proxy.sayHello("World") 时,实际上是调用了 MyInvocationHandlerinvoke() 方法。

四、Mapper XML:另一种选择

虽然 Mapper 接口配合注解的方式很方便,但是在某些情况下,我们可能还是需要使用 Mapper XML 文件。 比如:

  • SQL 语句太复杂,注解写不下。
  • 需要动态 SQL。
  • 需要使用 MyBatis 的高级特性,比如缓存。

4.1 Mapper XML 的基本结构

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="com.example.mapper.UserMapper">

    <select id="getUserById" parameterType="java.lang.Long" resultType="com.example.entity.User">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <insert id="insertUser" parameterType="com.example.entity.User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (name, age) VALUES (#{name}, #{age})
    </insert>

    <update id="updateUser" parameterType="com.example.entity.User">
        UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
    </update>

    <delete id="deleteUser" parameterType="java.lang.Long">
        DELETE FROM user WHERE id = #{id}
    </delete>

    <select id="getAllUsers" resultType="com.example.entity.User">
        SELECT * FROM user
    </select>

</mapper>
  • <mapper> 标签的 namespace 属性指定了 Mapper 接口的完整类名。
  • <select>, <insert>, <update>, <delete> 标签分别对应着查询、插入、更新和删除操作。
  • id 属性指定了 SQL 语句的唯一标识符,它必须与 Mapper 接口中的方法名相同。
  • parameterType 属性指定了输入参数的类型。
  • resultType 属性指定了返回结果的类型。
  • useGeneratedKeyskeyProperty 属性用于指定主键自增。

4.2 动态 SQL

Mapper XML 文件最强大的功能之一就是动态 SQL。 动态 SQL 允许你根据不同的条件,生成不同的 SQL 语句。

比如,我们可以根据用户的姓名和年龄来查询用户:

<select id="findUsersByNameAndAge" parameterType="map" resultType="com.example.entity.User">
    SELECT * FROM user
    <where>
        <if test="name != null and name != ''">
            AND name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

在这个例子中,<where> 标签会自动添加 WHERE 关键字。 <if> 标签会根据 test 属性的值来判断是否需要添加对应的条件。

五、Mapper 接口与 XML 的结合

我们可以将 Mapper 接口和 XML 文件结合起来使用。 只需要保证 Mapper 接口的方法名和 XML 文件中 SQL 语句的 id 属性相同即可。

六、总结:拥抱面向接口的编程思想

Mapper 接口是 MyBatis 中一个非常重要的概念。 它通过代理模式,实现了面向接口的编程,简化了代码的编写,提高了代码的可维护性。 无论是使用注解还是 XML 文件,Mapper 接口都能帮助你更好地管理 SQL 语句,提高开发效率。

表格总结:

特性 Mapper 接口(注解) Mapper XML 适用场景
简洁性 简单,直接在接口中定义 SQL 相对复杂,需要单独的 XML 文件 简单的 SQL 语句,不需要动态 SQL
灵活性 有限,不适合复杂的 SQL 和动态 SQL 非常灵活,支持复杂的 SQL 和动态 SQL,可以使用各种 MyBatis 的高级特性 复杂的 SQL 语句,需要动态 SQL,需要使用 MyBatis 的高级特性
可维护性 容易维护,代码集中 可能难以维护,需要同时维护接口和 XML 文件 代码量较少,团队成员熟悉 MyBatis
代码生成 可以配合 MyBatis Generator 自动生成代码 也可以配合 MyBatis Generator 自动生成代码 需要快速生成代码
事务管理 通过 SqlSession 管理事务 通过 SqlSession 管理事务 需要事务管理
学习成本 较低,容易上手 较高,需要熟悉 XML 语法和 MyBatis 的各种标签和属性 团队成员需要具备一定的 MyBatis 知识
性能 性能差异不大,取决于 SQL 语句的质量和 MyBatis 的配置 性能差异不大,取决于 SQL 语句的质量和 MyBatis 的配置 对性能要求较高的场景,需要仔细调优 SQL 语句和 MyBatis 的配置
调试 调试相对简单,可以直接在接口中查看 SQL 调试可能稍微麻烦,需要查看 XML 文件和 MyBatis 的日志 需要经常调试 SQL 语句
总结 适用于简单的 CRUD 操作,追求简洁和快速开发 适用于复杂的业务逻辑和需要动态 SQL 的场景,追求灵活性和可扩展性 根据实际情况选择,可以结合使用

希望通过今天的讲解,各位能够对 MyBatis 的 Mapper 接口和代理模式有更深入的了解。 记住,编程的道路是漫长的,但是只要你坚持学习,不断探索,就一定能够成为一名优秀的程序员! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注