SpringBoot整合MyBatis终极指南:从基础配置到多数据源实战

🌟博主介绍:Java、Python、js全栈开发 “多面手”,精通多种编程语言和技术,痴迷于人工智能领域。秉持着对技术的热爱与执着,持续探索创新,愿在此分享交流和学习,与大家共进步。
📖全栈开发环境搭建运行攻略:多语言一站式指南(环境搭建+运行+调试+发布+保姆级详解)
👉感兴趣的可以先收藏起来,希望帮助更多的人
在这里插入图片描述

SpringBoot整合MyBatis终极指南:从基础配置到多数据源实战

一、引言

1.1 编写目的

在当今的软件开发领域,数据持久化是一个至关重要的环节。SpringBoot作为一款轻量级的Java开发框架,凭借其快速开发、简化配置等特性,受到了广大开发者的青睐。而MyBatis则是一个优秀的持久层框架,它能够帮助开发者更方便地操作数据库。将SpringBoot与MyBatis进行整合,能够充分发挥两者的优势,提高开发效率和代码质量。本文旨在为技术开发人员提供一份全面、详细的SpringBoot整合MyBatis的指南,从基础配置到多数据源实战,涵盖了各个方面的知识和技巧,帮助开发者快速上手并掌握这一重要的技术。

1.2 目标受众

本文主要面向有一定Java基础,希望深入学习SpringBoot和MyBatis整合技术的开发人员。无论是初学者想要快速入门,还是有经验的开发者想要进一步提升技能,都能从本文中获得有价值的信息。

1.3 文章结构概述

本文将按照从基础到高级的顺序逐步展开,主要内容包括SpringBoot与MyBatis的简介、基础配置、常用注解与XML映射文件的使用、多表查询与关联映射实战、动态SQL的使用与优化、分页查询与性能调优等基础内容,以及SpringBoot整合MyBatis多数据源配置、多数据源事务管理等高级内容。最后还会给出具体的代码示例与项目实战,并对常见问题进行分析和解答。通过这样的结构安排,读者可以循序渐进地学习和掌握SpringBoot整合MyBatis的相关知识和技能。

二、SpringBoot与MyBatis简介

2.1 SpringBoot简介

2.1.1 SpringBoot的定义

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程。该框架使用了特定的配置方式,使得开发人员可以摆脱传统Spring项目中繁琐的XML配置,以一种更加简洁、高效的方式来构建独立的、生产级别的Spring应用程序。

2.1.2 SpringBoot的特点
  • 快速搭建项目:SpringBoot提供了Spring Initializr,开发人员可以通过网页或者Spring Tool Suite、IntelliJ IDEA等开发工具快速创建一个SpringBoot项目,无需手动配置大量的依赖和文件。
  • 自动配置:SpringBoot能够根据项目中添加的依赖自动进行配置,例如当项目中引入了Spring Data JPA和H2数据库的依赖时,SpringBoot会自动配置数据源和JPA相关的配置,大大减少了开发人员的配置工作量。
  • 嵌入式服务器:SpringBoot内置了Tomcat、Jetty等嵌入式服务器,开发人员可以直接将应用打包成可执行的JAR文件,通过命令行启动,无需额外部署到外部服务器。
  • 生产就绪:SpringBoot提供了一系列的生产级别的特性,如健康检查、指标监控、外部化配置等,方便开发人员在生产环境中对应用进行管理和监控。
2.1.3 SpringBoot的应用场景

SpringBoot适用于各种类型的Java应用开发,尤其是微服务架构。在微服务架构中,每个服务都是一个独立的应用程序,SpringBoot的快速搭建和自动配置特性使得开发人员可以快速开发和部署这些微服务。此外,SpringBoot也适用于传统的Web应用开发、企业级应用开发等场景。

2.2 MyBatis简介

2.2.1 MyBatis的定义

MyBatis是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJO(Plain Old Java Objects,普通老式Java对象)映射成数据库中的记录。

2.2.2 MyBatis的特点
  • 灵活的SQL编写:MyBatis允许开发人员直接编写SQL语句,开发人员可以根据具体的业务需求灵活地编写和优化SQL,这对于一些复杂的查询和业务逻辑非常有用。
  • 强大的映射功能:MyBatis提供了丰富的映射方式,包括基本类型映射、对象映射、关联映射等,可以将数据库中的记录映射到Java对象中,方便开发人员进行业务处理。
  • 支持多种数据库:MyBatis可以与多种数据库进行集成,如MySQL、Oracle、SQL Server等,开发人员可以根据项目的需求选择合适的数据库。
  • 轻量级:MyBatis的核心代码非常轻量级,对系统资源的占用较少,适合在各种环境中使用。
2.2.3 MyBatis的应用场景

MyBatis适用于对SQL性能有较高要求、业务逻辑复杂的项目。在这些项目中,开发人员需要根据具体的业务需求编写复杂的SQL语句,MyBatis的灵活SQL编写特性可以满足这一需求。此外,MyBatis也适用于需要与多种数据库进行集成的项目,其支持多种数据库的特性可以方便开发人员进行数据库的切换和管理。

三、SpringBoot整合MyBatis基础配置

3.1 创建SpringBoot项目

3.1.1 使用Spring Initializr创建项目

Spring Initializr是一个基于Web的工具,可帮助我们快速搭建Spring Boot项目。以下是创建项目的步骤:

  1. 打开浏览器,访问 https://start.spring.io/
  2. 在该页面中,进行如下配置:
    • Project:选择 Maven Project,因为Maven是一个流行的项目管理和构建工具,能方便地管理项目依赖。
    • Language:选择 Java
    • Spring Boot:选择合适的版本,这里建议选择较稳定的版本,如 3.x 系列。
    • Group:填写项目的包名,例如 com.example
    • Artifact:填写项目名称,例如 springboot-mybatis-demo
    • Dependencies:添加以下依赖:
      • Spring Web:用于构建Web应用。
      • MyBatis Framework:MyBatis的Spring Boot集成依赖。
      • MySQL Driver:如果使用MySQL数据库,需要添加此依赖。若使用其他数据库,需选择相应的数据库驱动。
  3. 点击 Generate 按钮,会下载一个压缩包,解压后即可得到一个基本的Spring Boot项目结构。
3.1.2 使用IDE创建项目

以IntelliJ IDEA为例:

  1. 打开IntelliJ IDEA,选择 File -> New -> Project
  2. 在左侧面板选择 Spring Initializr,设置好SDK版本等信息,点击 Next
  3. 按照上述Spring Initializr的配置步骤设置 GroupArtifact 等信息,点击 Next
  4. 添加所需依赖,点击 Next,最后点击 Finish 完成项目创建。

3.2 配置数据库连接

3.2.1 在 application.properties 中配置

src/main/resources 目录下找到 application.properties 文件,添加以下配置:

# 数据库连接URL,这里以MySQL为例
spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# 数据库用户名
spring.datasource.username=your_username
# 数据库密码
spring.datasource.password=your_password
# 数据库驱动类
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
3.2.2 在 application.yml 中配置

如果更喜欢使用YAML格式,也可以在 src/main/resources 目录下创建或修改 application.yml 文件,添加如下配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_database_name?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: your_username
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

3.3 配置MyBatis

3.3.1 添加MyBatis配置

application.propertiesapplication.yml 中添加MyBatis相关配置:

  • 使用 application.properties
# 指定Mapper XML文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
# 指定实体类的别名包
mybatis.type-aliases-package=com.example.demo.entity
  • 使用 application.yml
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.demo.entity
3.3.2 创建Mapper接口

src/main/java 下创建Mapper接口,例如创建一个 UserMapper 接口:

package com.example.demo.mapper;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface UserMapper {
    // 查询所有用户
    List<User> getAllUsers();
}
3.3.3 创建Mapper XML文件

src/main/resources/mapper 目录下创建 UserMapper.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.demo.mapper.UserMapper">
    <select id="getAllUsers" resultType="User">
        SELECT * FROM user
    </select>
</mapper>

3.4 创建实体类

src/main/java 下创建与数据库表对应的实体类,例如 User 类:

package com.example.demo.entity;

public class User {
    private Integer id;
    private String name;
    private Integer age;

    // 构造方法、Getter和Setter方法
    public User() {}

    public User(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

3.5 创建Service层和Controller层

3.5.1 创建Service层

创建 UserService 接口和其实现类 UserServiceImpl

// UserService接口
package com.example.demo.service;

import com.example.demo.entity.User;
import java.util.List;

public interface UserService {
    List<User> getAllUsers();
}

// UserServiceImpl实现类
package com.example.demo.service.impl;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public List<User> getAllUsers() {
        return userMapper.getAllUsers();
    }
}
3.5.2 创建Controller层

创建 UserController 类来处理HTTP请求:

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }
}

3.6 测试项目

启动Spring Boot应用程序,在浏览器中访问 http://localhost:8080/users,如果配置正确,应该可以看到数据库中 user 表的所有记录以JSON格式返回。

四、MyBatis常用注解与XML映射文件使用

4.1 MyBatis常用注解介绍

4.1.1 @Select注解

@Select 注解用于执行查询操作。它可以直接在Mapper接口的方法上定义SQL查询语句,避免了使用XML映射文件。以下是一个简单的示例:

import org.apache.ibatis.annotations.Select;
import com.example.demo.entity.User;
import java.util.List;

public interface UserMapper {
    @Select("SELECT * FROM user")
    List<User> getAllUsers();
}

在上述代码中,@Select 注解中的SQL语句会查询 user 表中的所有记录,并将结果映射到 User 实体类的列表中。

4.1.2 @Insert注解

@Insert 注解用于执行插入操作。它可以将Java对象中的数据插入到数据库表中。示例如下:

import org.apache.ibatis.annotations.Insert;
import com.example.demo.entity.User;

public interface UserMapper {
    @Insert("INSERT INTO user (name, age) VALUES (#{name}, #{age})")
    int insertUser(User user);
}

这里的 #{name}#{age} 是MyBatis的占位符,会自动将 User 对象中的 nameage 属性值填充到SQL语句中。insertUser 方法返回一个整数,表示插入操作影响的行数。

4.1.3 @Update注解

@Update 注解用于执行更新操作。可以根据条件更新数据库表中的记录。示例如下:

import org.apache.ibatis.annotations.Update;
import com.example.demo.entity.User;

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

此代码会根据 User 对象的 id 属性更新相应记录的 nameage 字段。

4.1.4 @Delete注解

@Delete 注解用于执行删除操作。可以根据条件删除数据库表中的记录。示例如下:

import org.apache.ibatis.annotations.Delete;

public interface UserMapper {
    @Delete("DELETE FROM user WHERE id = #{id}")
    int deleteUserById(int id);
}

该方法会根据传入的 id 删除 user 表中对应的记录。

4.1.5 @Results和@Result注解

@Results@Result 注解用于处理结果集映射。当数据库表的字段名和实体类的属性名不一致时,可以使用这两个注解进行手动映射。示例如下:

import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Result;
import com.example.demo.entity.User;

public interface UserMapper {
    @Select("SELECT id, user_name, user_age FROM user")
    @Results({
        @Result(property = "id", column = "id"),
        @Result(property = "name", column = "user_name"),
        @Result(property = "age", column = "user_age")
    })
    List<User> getUsersWithMapping();
}

在上述代码中,@Results 注解包含了多个 @Result 注解,分别将数据库表的 user_name 字段映射到实体类的 name 属性,user_age 字段映射到 age 属性。

4.2 XML映射文件使用

4.2.1 XML映射文件结构

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.demo.mapper.UserMapper">
    <!-- SQL语句和映射配置 -->
</mapper>

其中,namespace 属性指定了该映射文件对应的Mapper接口的全限定名。

4.2.2 查询语句配置

在XML映射文件中配置查询语句,示例如下:

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

id 属性对应Mapper接口中的方法名,resultType 属性指定查询结果的映射类型。

4.2.3 插入语句配置

配置插入语句的示例:

<mapper namespace="com.example.demo.mapper.UserMapper">
    <insert id="insertUser" parameterType="com.example.demo.entity.User">
        INSERT INTO user (name, age) VALUES (#{name}, #{age})
    </insert>
</mapper>

parameterType 属性指定传入参数的类型。

4.2.4 更新语句配置

配置更新语句的示例:

<mapper namespace="com.example.demo.mapper.UserMapper">
    <update id="updateUser" parameterType="com.example.demo.entity.User">
        UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
    </update>
</mapper>
4.2.5 删除语句配置

配置删除语句的示例:

<mapper namespace="com.example.demo.mapper.UserMapper">
    <delete id="deleteUserById" parameterType="int">
        DELETE FROM user WHERE id = #{id}
    </delete>
</mapper>
4.2.6 结果映射配置

当数据库表字段和实体类属性名不一致时,可使用 <resultMap> 进行结果映射,示例如下:

<mapper namespace="com.example.demo.mapper.UserMapper">
    <resultMap id="userResultMap" type="com.example.demo.entity.User">
        <id property="id" column="id"/>
        <result property="name" column="user_name"/>
        <result property="age" column="user_age"/>
    </resultMap>
    <select id="getUsersWithMapping" resultMap="userResultMap">
        SELECT id, user_name, user_age FROM user
    </select>
</mapper>

<resultMap> 中定义了字段和属性的映射关系,id 标签用于标识主键,result 标签用于普通字段的映射。

4.3 注解与XML映射文件的选择

4.3.1 注解的优势
  • 简洁性:对于简单的SQL操作,使用注解可以直接在Mapper接口的方法上定义SQL语句,代码更加简洁,减少了额外的XML文件。
  • 可读性:注解与方法紧密结合,开发人员可以直观地看到方法对应的SQL操作,提高了代码的可读性。
4.3.2 XML映射文件的优势
  • 复杂SQL处理:对于复杂的SQL语句,如动态SQL、多表关联查询等,XML映射文件可以更方便地进行编写和维护。
  • 可维护性:当项目规模较大时,将SQL语句集中在XML文件中,便于统一管理和修改。
4.3.3 选择建议
  • 对于简单的CRUD操作,可以优先考虑使用注解,以提高开发效率。
  • 对于复杂的SQL操作,特别是涉及动态SQL和多表关联查询的情况,建议使用XML映射文件。

五、多表查询与关联映射实战

5.1 数据库表结构设计

在进行多表查询与关联映射实战之前,我们需要设计合适的数据库表结构。这里以一个简单的订单系统为例,设计用户表(user)、订单表(order)和商品表(product)。

5.1.1 用户表(user)

用户表存储用户的基本信息,表结构如下:

字段名类型描述
idint用户ID,主键
namevarchar(50)用户姓名
ageint用户年龄

创建用户表的SQL语句如下:

CREATE TABLE user (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    age INT
);
5.1.2 订单表(order)

订单表记录用户的订单信息,包含用户ID和商品ID,用于关联用户和商品。表结构如下:

字段名类型描述
idint订单ID,主键
user_idint用户ID,外键关联用户表的id
product_idint商品ID,外键关联商品表的id
order_datedatetime订单日期

创建订单表的SQL语句如下:

CREATE TABLE `order` (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_id INT,
    order_date DATETIME,
    FOREIGN KEY (user_id) REFERENCES user(id),
    FOREIGN KEY (product_id) REFERENCES product(id)
);
5.1.3 商品表(product)

商品表存储商品的基本信息,表结构如下:

字段名类型描述
idint商品ID,主键
namevarchar(100)商品名称
pricedecimal(10, 2)商品价格

创建商品表的SQL语句如下:

CREATE TABLE product (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    price DECIMAL(10, 2)
);

5.2 实体类设计

根据上述数据库表结构,设计对应的实体类。

5.2.1 用户实体类(User)
package com.example.demo.entity;

import java.util.List;

public class User {
    private Integer id;
    private String name;
    private Integer age;
    private List<Order> orders;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public List<Order> getOrders() {
        return orders;
    }

    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", orders=" + orders +
                '}';
    }
}
5.2.2 订单实体类(Order)
package com.example.demo.entity;

public class Order {
    private Integer id;
    private User user;
    private Product product;
    private java.util.Date orderDate;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    public Product getProduct() {
        return product;
    }

    public void setProduct(Product product) {
        this.product = product;
    }

    public java.util.Date getOrderDate() {
        return orderDate;
    }

    public void setOrderDate(java.util.Date orderDate) {
        this.orderDate = orderDate;
    }

    @Override
    public String toString() {
        return "Order{" +
                "id=" + id +
                ", user=" + user +
                ", product=" + product +
                ", orderDate=" + orderDate +
                '}';
    }
}
5.2.3 商品实体类(Product)
package com.example.demo.entity;

import java.math.BigDecimal;

public class Product {
    private Integer id;
    private String name;
    private BigDecimal price;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

5.3 多表查询实现

5.3.1 使用XML映射文件实现多表查询

UserMapper.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.demo.mapper.UserMapper">
    <resultMap id="UserWithOrdersResultMap" type="com.example.demo.entity.User">
        <id property="id" column="user_id"/>
        <result property="name" column="user_name"/>
        <result property="age" column="user_age"/>
        <collection property="orders" ofType="com.example.demo.entity.Order">
            <id property="id" column="order_id"/>
            <result property="orderDate" column="order_date"/>
            <association property="product" javaType="com.example.demo.entity.Product">
                <id property="id" column="product_id"/>
                <result property="name" column="product_name"/>
                <result property="price" column="product_price"/>
            </association>
        </collection>
    </resultMap>

    <select id="getUserWithOrders" resultMap="UserWithOrdersResultMap">
        SELECT 
            u.id AS user_id,
            u.name AS user_name,
            u.age AS user_age,
            o.id AS order_id,
            o.order_date,
            p.id AS product_id,
            p.name AS product_name,
            p.price AS product_price
        FROM 
            user u
        LEFT JOIN 
            `order` o ON u.id = o.user_id
        LEFT JOIN 
            product p ON o.product_id = p.id
    </select>
</mapper>

UserMapper 接口中定义对应的方法:

package com.example.demo.mapper;

import com.example.demo.entity.User;
import java.util.List;

public interface UserMapper {
    List<User> getUserWithOrders();
}
5.3.2 使用注解实现多表查询

使用注解实现相同的多表查询功能,代码如下:

package com.example.demo.mapper;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;

public interface UserMapper {
    @Results(id = "UserWithOrdersResult", value = {
            @Result(property = "id", column = "user_id"),
            @Result(property = "name", column = "user_name"),
            @Result(property = "age", column = "user_age"),
            @Result(property = "orders", column = "user_id", many = @Many(select = "com.example.demo.mapper.OrderMapper.getOrdersByUserId"))
    })
    @Select("SELECT id AS user_id, name AS user_name, age AS user_age FROM user")
    List<User> getUserWithOrders();
}

OrderMapper 接口代码如下:

package com.example.demo.mapper;

import com.example.demo.entity.Order;
import org.apache.ibatis.annotations.*;
import java.util.List;

public interface OrderMapper {
    @Results(value = {
            @Result(property = "id", column = "order_id"),
            @Result(property = "orderDate", column = "order_date"),
            @Result(property = "product", column = "product_id", one = @One(select = "com.example.demo.mapper.ProductMapper.getProductById"))
    })
    @Select("SELECT id AS order_id, order_date, product_id FROM `order` WHERE user_id = #{userId}")
    List<Order> getOrdersByUserId(int userId);
}

ProductMapper 接口代码如下:

package com.example.demo.mapper;

import com.example.demo.entity.Product;
import org.apache.ibatis.annotations.*;

public interface ProductMapper {
    @Results(value = {
            @Result(property = "id", column = "product_id"),
            @Result(property = "name", column = "product_name"),
            @Result(property = "price", column = "product_price")
    })
    @Select("SELECT id AS product_id, name AS product_name, price AS product_price FROM product WHERE id = #{productId}")
    Product getProductById(int productId);
}

5.4 关联映射详解

5.4.1 一对一关联映射

在上述示例中,订单(Order)和商品(Product)之间是一对一关联关系。在 OrderMapper 中使用 @One 注解来实现一对一关联映射:

@Result(property = "product", column = "product_id", one = @One(select = "com.example.demo.mapper.ProductMapper.getProductById"))

这里 column = "product_id" 表示根据订单表中的 product_id 字段去查询对应的商品信息,@One 注解中的 select 属性指定了调用的查询方法。

5.4.2 一对多关联映射

用户(User)和订单(Order)之间是一对多关联关系。在 UserMapper 中使用 @Many 注解来实现一对多关联映射:

@Result(property = "orders", column = "user_id", many = @Many(select = "com.example.demo.mapper.OrderMapper.getOrdersByUserId"))

column = "user_id" 表示根据用户表中的 id 字段去查询该用户的所有订单信息,@Many 注解中的 select 属性指定了调用的查询方法。

5.4.3 多对多关联映射

在本示例中虽然没有直接体现多对多关联映射,但在实际应用中,比如学生和课程的关系,一个学生可以选多门课程,一门课程可以被多个学生选。通常会通过一个中间表来实现多对多关联,在MyBatis中可以通过多次查询和关联映射来处理这种关系。

5.5 测试多表查询与关联映射

UserService 中调用 UserMappergetUserWithOrders 方法进行测试:

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public List<User> getUserWithOrders() {
        return userMapper.getUserWithOrders();
    }
}

UserController 中编写测试接口:

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users/with-orders")
    public List<User> getUserWithOrders() {
        return userService.getUserWithOrders();
    }
}

启动Spring Boot应用,访问 http://localhost:8080/users/with-orders,可以看到包含用户及其订单信息的结果。

六、动态SQL的使用与优化

6.1 动态SQL概述

在实际的数据库操作中,我们经常需要根据不同的条件来生成不同的SQL语句。例如,根据用户输入的查询条件动态生成查询语句,或者根据业务逻辑动态拼接插入、更新语句等。MyBatis提供了强大的动态SQL功能,允许我们在XML映射文件或使用注解时根据不同的条件灵活地构建SQL语句。动态SQL可以提高代码的复用性和灵活性,避免硬编码带来的问题。

6.2 MyBatis动态SQL标签

6.2.1 <if> 标签

<if> 标签是最常用的动态SQL标签之一,它用于根据条件判断是否包含某部分SQL语句。以下是一个在查询语句中使用 <if> 标签的示例:

<select id="getUserList" resultType="com.example.demo.entity.User">
    SELECT * FROM user
    WHERE 1 = 1
    <if test="name != null and name != ''">
        AND name = #{name}
    </if>
    <if test="age != null">
        AND age = #{age}
    </if>
</select>

在上述代码中,test 属性是一个OGNL(Object Graph Navigation Language)表达式,用于判断条件是否成立。如果 name 不为空字符串且不为 null,则会在SQL语句中添加 AND name = #{name} 条件;如果 age 不为 null,则会添加 AND age = #{age} 条件。

6.2.2 <where> 标签

<where> 标签可以智能地处理 WHERE 子句。当使用 <where> 标签时,如果 <where> 标签内的条件都不成立,它不会添加 WHERE 关键字;如果有条件成立,它会自动去掉第一个条件前面多余的 ANDOR。对上面的示例进行修改:

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

这样就避免了手动处理 WHERE 子句可能出现的语法错误。

6.2.3 <set> 标签

<set> 标签通常用于动态更新语句中,它可以智能地处理 SET 子句。在更新语句中,如果某些字段不需要更新,使用 <set> 标签可以避免出现多余的逗号。示例如下:

<update id="updateUser" parameterType="com.example.demo.entity.User">
    UPDATE user
    <set>
        <if test="name != null and name != ''">
            name = #{name},
        </if>
        <if test="age != null">
            age = #{age}
        </if>
    </set>
    WHERE id = #{id}
</update>

如果 name 不为空字符串且不为 null,会更新 name 字段;如果 age 不为 null,会更新 age 字段,<set> 标签会自动处理好逗号的问题。

6.2.4 <choose><when><otherwise> 标签

<choose><when><otherwise> 标签组合使用可以实现类似 switch - case 的功能。在多个条件中选择一个满足的条件执行。示例如下:

<select id="getUserByCondition" resultType="com.example.demo.entity.User">
    SELECT * FROM user
    WHERE 1 = 1
    <choose>
        <when test="name != null and name != ''">
            AND name = #{name}
        </when>
        <when test="age != null">
            AND age = #{age}
        </when>
        <otherwise>
            AND id > 0
        </otherwise>
    </choose>
</select>

MyBatis会依次判断 <when> 标签中的条件,当某个条件成立时,执行该 <when> 标签内的SQL语句,后面的 <when> 标签不再判断;如果所有 <when> 标签的条件都不成立,则执行 <otherwise> 标签内的SQL语句。

6.2.5 <foreach> 标签

<foreach> 标签用于遍历集合或数组,常用于批量插入、批量删除或查询条件为多个值的情况。以下是一个批量删除用户的示例:

<delete id="deleteUsersByIds" parameterType="java.util.List">
    DELETE FROM user
    WHERE id IN
    <foreach item="id" index="index" collection="list"
             open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

在上述代码中,collection 属性指定要遍历的集合,item 属性指定集合中每个元素的别名,open 属性指定遍历开始时的字符,separator 属性指定元素之间的分隔符,close 属性指定遍历结束时的字符。

6.3 动态SQL的性能优化

6.3.1 避免过多的动态SQL嵌套

过多的动态SQL嵌套会使代码变得复杂,难以维护,同时也会影响性能。尽量将复杂的动态SQL拆分成多个简单的SQL语句,通过Java代码进行逻辑控制。例如,将一个包含多个嵌套 <if> 标签的复杂查询拆分成几个简单的查询,根据不同的条件分别调用不同的查询方法。

6.3.2 合理使用缓存

MyBatis提供了一级缓存和二级缓存机制。对于一些不经常变化的数据查询,可以开启缓存以提高性能。一级缓存是基于SqlSession的,同一个SqlSession中执行相同的查询会直接从缓存中获取结果;二级缓存是基于Mapper的,多个SqlSession可以共享二级缓存。在配置文件中可以通过以下方式开启二级缓存:

<mapper namespace="com.example.demo.mapper.UserMapper">
    <cache/>
    <!-- SQL语句 -->
</mapper>
6.3.3 优化动态SQL语句

在编写动态SQL时,要注意SQL语句的性能。避免使用 SELECT *,尽量只查询需要的字段;合理使用索引,确保查询条件字段上有合适的索引;避免在 WHERE 子句中使用函数,因为这可能会导致索引失效。例如,将 WHERE YEAR(create_time) = 2024 改为 WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'

6.4 动态SQL的实际应用案例

以下是一个综合使用动态SQL标签实现用户查询的完整示例:

6.4.1 Mapper接口
package com.example.demo.mapper;

import com.example.demo.entity.User;
import java.util.List;

public interface UserMapper {
    List<User> getUserList(User user);
}
6.4.2 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.demo.mapper.UserMapper">
    <select id="getUserList" resultType="com.example.demo.entity.User">
        SELECT * FROM user
        <where>
            <if test="name != null and name != ''">
                AND name LIKE CONCAT('%', #{name}, '%')
            </if>
            <if test="age != null">
                AND age = #{age}
            </if>
        </where>
    </select>
</mapper>
6.4.3 Service层
package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public List<User> getUserList(User user) {
        return userMapper.getUserList(user);
    }
}
6.4.4 Controller层
package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public List<User> getUserList(@RequestParam(required = false) String name,
                                  @RequestParam(required = false) Integer age) {
        User user = new User();
        user.setName(name);
        user.setAge(age);
        return userService.getUserList(user);
    }
}

通过上述代码,我们可以根据用户传入的 nameage 条件动态生成查询语句,实现灵活的用户查询功能。

七、分页查询与性能调优

7.1 分页查询的概念与需求

7.1.1 分页查询的定义

分页查询是指将数据库中的大量数据按照一定的规则分成多个页面进行展示,每次只查询和显示其中的一部分数据。例如,在一个包含上万条记录的用户表中,若不进行分页,一次性将所有数据查询出来会占用大量的内存和网络带宽,还会导致响应时间过长。通过分页查询,我们可以将数据分成每页10条、20条等,用户每次只请求当前页面的数据,提高了系统的性能和用户体验。

7.1.2 分页查询的应用场景

在各种Web应用和管理系统中,分页查询广泛应用于数据列表展示的场景,如电商平台的商品列表、新闻网站的文章列表、后台管理系统的用户列表等。这些场景通常需要展示大量的数据,分页查询可以有效地解决数据展示和性能之间的矛盾。

7.2 MyBatis实现分页查询

7.2.1 使用Limit关键字实现分页

在MySQL数据库中,可以使用 LIMIT 关键字来实现分页查询。LIMIT 关键字接受两个参数,第一个参数表示偏移量(即从第几条记录开始查询),第二个参数表示查询的记录数。以下是一个使用 LIMIT 实现分页查询的示例:

<select id="getUserListByPage" resultType="com.example.demo.entity.User">
    SELECT * FROM user
    LIMIT #{offset}, #{pageSize}
</select>

在Mapper接口中定义方法:

import com.example.demo.entity.User;
import java.util.List;

public interface UserMapper {
    List<User> getUserListByPage(int offset, int pageSize);
}

在Service层调用该方法时,需要计算偏移量。例如,若每页显示10条记录,要查询第2页的数据,偏移量为 (2 - 1) * 10 = 10

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public List<User> getUserListByPage(int pageNum, int pageSize) {
        int offset = (pageNum - 1) * pageSize;
        return userMapper.getUserListByPage(offset, pageSize);
    }
}
7.2.2 使用MyBatis分页插件(PageHelper)

PageHelper 是一个常用的MyBatis分页插件,它可以简化分页查询的实现。使用步骤如下:

  1. 添加依赖:在 pom.xml 中添加 PageHelper 的依赖。
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>
  1. 在Mapper接口中定义普通的查询方法:
import com.example.demo.entity.User;
import java.util.List;

public interface UserMapper {
    List<User> getAllUsers();
}
  1. 在Service层使用 PageHelper 进行分页:
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public PageInfo<User> getUserListByPage(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<User> userList = userMapper.getAllUsers();
        return new PageInfo<>(userList);
    }
}

PageHelper.startPage(pageNum, pageSize) 方法会自动拦截后续的查询语句,并根据传入的页码和每页记录数进行分页查询。PageInfo 类封装了分页查询的结果,包含了当前页码、每页记录数、总记录数、总页数等信息。

7.3 分页查询的性能调优

7.3.1 索引优化

在进行分页查询时,合理的索引可以显著提高查询性能。对于经常用于查询条件和排序的字段,应该创建索引。例如,在上述用户表的分页查询中,如果经常根据用户的创建时间进行排序和分页,可以在 create_time 字段上创建索引:

CREATE INDEX idx_create_time ON user (create_time);

这样可以加快查询速度,尤其是在数据量较大的情况下。

7.3.2 避免大偏移量查询

当偏移量很大时,使用 LIMIT 关键字进行分页查询会导致性能下降。因为数据库需要跳过前面的大量记录,再返回所需的数据。例如,查询第1000页的数据,数据库需要先跳过前面的9990条记录。可以通过记录上次查询的最大ID,下次查询时从该ID开始,避免大偏移量查询。示例如下:

<select id="getUserListByLastId" resultType="com.example.demo.entity.User">
    SELECT * FROM user
    WHERE id > #{lastId}
    LIMIT #{pageSize}
</select>
7.3.3 缓存优化

对于一些不经常变化的数据,可以使用缓存来减少数据库查询次数。例如,使用Redis缓存分页查询的结果。当用户请求相同的分页数据时,先从缓存中获取,如果缓存中不存在,再查询数据库,并将结果存入缓存。以下是一个简单的示例:

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class CachedUserService {
    @Autowired
    private UserService userService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public List<User> getUserListByPage(int pageNum, int pageSize) {
        String key = "user_list_page_" + pageNum + "_" + pageSize;
        List<User> userList = (List<User>) redisTemplate.opsForValue().get(key);
        if (userList == null) {
            userList = userService.getUserListByPage(pageNum, pageSize);
            redisTemplate.opsForValue().set(key, userList);
        }
        return userList;
    }
}

7.4 性能监控与分析

7.4.1 使用MyBatis日志进行性能监控

MyBatis提供了日志功能,可以通过配置日志级别来监控SQL语句的执行情况。在 application.properties 中配置日志级别:

logging.level.com.example.demo.mapper=DEBUG

将Mapper接口所在的包的日志级别设置为 DEBUG,这样在控制台会输出详细的SQL执行信息,包括SQL语句、参数和执行时间等,方便我们分析性能问题。

7.4.2 使用数据库自带的性能分析工具

不同的数据库都提供了各自的性能分析工具,如MySQL的 EXPLAIN 关键字可以用于分析SQL语句的执行计划,帮助我们了解数据库是如何执行查询的,是否使用了索引等。例如:

EXPLAIN SELECT * FROM user LIMIT 10, 20;

通过分析 EXPLAIN 的结果,可以发现查询中存在的性能问题,并进行相应的优化。

7.4.3 使用第三方性能监控工具

除了数据库自带的工具,还可以使用第三方性能监控工具,如Druid。Druid是一个开源的数据库连接池和监控工具,它可以对数据库连接、SQL执行等进行实时监控和统计分析。在Spring Boot项目中集成Druid,只需要在 pom.xml 中添加依赖,并在 application.properties 中进行相应的配置即可。

八、SpringBoot整合MyBatis多数据源配置

8.1 多数据源应用场景

在实际的项目开发中,有许多场景需要使用多数据源。以下是一些常见的情况:

8.1.1 数据分离

随着业务的发展,数据库中的数据量会不断增大。为了提高系统的性能和可维护性,常常会将不同类型的数据存储在不同的数据库中。例如,将核心业务数据和日志数据分别存储在不同的数据库里。核心业务数据库需要保证高可用性和高性能,而日志数据库则主要用于数据的存储和分析。

8.1.2 读写分离

在高并发的系统中,为了减轻主数据库的压力,提高系统的读写性能,通常会采用读写分离的架构。主数据库负责处理写操作,从数据库负责处理读操作。这样可以将读操作分散到多个从数据库上,从而提高系统的整体性能。

8.1.3 多租户系统

在多租户系统中,每个租户可能有自己独立的数据库,以保证数据的隔离性和安全性。系统需要根据不同的租户请求,动态地切换到相应的数据库进行操作。

8.2 配置多数据源的准备工作

8.2.1 添加依赖

pom.xml 文件中添加必要的依赖,除了之前使用的 Spring Boot 和 MyBatis 相关依赖外,还需要添加数据库连接池依赖,这里以 HikariCP 为例:

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MyBatis Spring Boot Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- HikariCP 连接池 -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
    </dependency>
</dependencies>
8.2.2 配置数据源信息

application.propertiesapplication.yml 中配置多个数据源的连接信息。以下是使用 application.yml 的示例:

spring:
  datasource:
    primary:
      url: jdbc:mysql://localhost:3306/primary_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      url: jdbc:mysql://localhost:3306/secondary_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

8.3 手动配置多数据源

8.3.1 创建数据源配置类

创建一个数据源配置类,用于配置多个数据源。以下是示例代码:

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }
}

在上述代码中,@Primary 注解表示该数据源是主数据源,当没有明确指定使用哪个数据源时,会默认使用主数据源。

8.3.2 创建多数据源的 SqlSessionFactory

为每个数据源创建对应的 SqlSessionFactory。以下是示例代码:

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryMyBatisConfig {

    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Bean(name = "primarySqlSessionFactory")
    public SqlSessionFactory primarySqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(primaryDataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/primary/*.xml"));
        return factoryBean.getObject();
    }
}

@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory")
public class SecondaryMyBatisConfig {

    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;

    @Bean(name = "secondarySqlSessionFactory")
    public SqlSessionFactory secondarySqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(secondaryDataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/secondary/*.xml"));
        return factoryBean.getObject();
    }
}

在上述代码中,@MapperScan 注解用于指定 Mapper 接口所在的包和对应的 SqlSessionFactory

8.3.3 创建 Mapper 接口和 XML 文件

根据不同的数据源,分别创建对应的 Mapper 接口和 XML 文件。例如,在 com.example.demo.mapper.primary 包下创建主数据源的 Mapper 接口,在 com.example.demo.mapper.secondary 包下创建从数据源的 Mapper 接口。同时,在 classpath:mapper/primaryclasspath:mapper/secondary 目录下分别创建对应的 XML 文件。

8.4 使用 AOP 动态切换数据源

8.4.1 创建数据源上下文类

创建一个数据源上下文类,用于存储和获取当前使用的数据源。

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String key) {
        contextHolder.set(key);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}
8.4.2 创建数据源路由类

创建一个数据源路由类,继承自 AbstractRoutingDataSource,用于根据当前的数据源上下文动态切换数据源。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.Map;

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}
8.4.3 配置动态数据源

修改数据源配置类,使用动态数据源。

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean
    @Primary
    public AbstractRoutingDataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
                                                       @Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource(primaryDataSource, new HashMap<>());
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("primary", primaryDataSource);
        targetDataSources.put("secondary", secondaryDataSource);
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}
8.4.4 创建 AOP 切面类

创建一个 AOP 切面类,根据方法上的注解动态切换数据源。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(com.example.demo.annotation.DataSource)")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        com.example.demo.annotation.DataSource dataSource = method.getAnnotation(com.example.demo.annotation.DataSource.class);
        if (dataSource != null) {
            DataSourceContextHolder.setDataSource(dataSource.value());
        }
    }

    @After("@annotation(com.example.demo.annotation.DataSource)")
    public void after(JoinPoint point) {
        DataSourceContextHolder.clearDataSource();
    }
}
8.4.5 创建数据源注解

创建一个数据源注解,用于标记方法使用的数据源。

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "primary";
}

8.5 测试多数据源配置

创建一个 Service 类,使用 @DataSource 注解切换数据源进行测试。

import com.example.demo.annotation.DataSource;
import com.example.demo.mapper.primary.PrimaryUserMapper;
import com.example.demo.mapper.secondary.SecondaryUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private PrimaryUserMapper primaryUserMapper;

    @Autowired
    private SecondaryUserMapper secondaryUserMapper;

    @DataSource("primary")
    public void usePrimaryDataSource() {
        // 调用主数据源的 Mapper 方法
        primaryUserMapper.getAllUsers();
    }

    @DataSource("secondary")
    public void useSecondaryDataSource() {
        // 调用从数据源的 Mapper 方法
        secondaryUserMapper.getAllUsers();
    }
}

九、多数据源事务管理

9.1 多数据源事务管理的挑战

9.1.1 数据一致性问题

在单数据源环境下,事务可以保证一组数据库操作要么全部成功,要么全部失败,从而保证数据的一致性。但在多数据源环境中,不同数据源可能分布在不同的物理服务器上,它们之间没有直接的事务协调机制。例如,在一个业务操作中,需要同时向主数据库和从数据库插入数据,如果主数据库插入成功,而从数据库插入失败,就会导致数据不一致。

9.1.2 事务传播问题

事务传播行为定义了在多个事务方法嵌套调用时,事务如何传播。在多数据源场景下,不同数据源的事务传播行为可能会相互影响。例如,一个方法调用了两个不同数据源的事务方法,如何保证这两个事务的正确传播和回滚是一个挑战。

9.1.3 性能开销问题

为了保证多数据源事务的一致性,通常需要引入额外的协调机制,这会带来一定的性能开销。例如,使用分布式事务解决方案时,需要进行事务的协调和同步,会增加系统的响应时间和资源消耗。

9.2 多数据源事务管理方案

9.2.1 本地事务管理

本地事务是基于单个数据库连接的事务管理,在多数据源场景下,可以为每个数据源单独配置本地事务管理器。

  • 配置事务管理器
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;

@Configuration
public class TransactionManagerConfig {

    @Bean(name = "primaryTransactionManager")
    public DataSourceTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        return new DataSourceTransactionManager(primaryDataSource);
    }

    @Bean(name = "secondaryTransactionManager")
    public DataSourceTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new DataSourceTransactionManager(secondaryDataSource);
    }
}
  • 使用本地事务
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.mapper.primary.PrimaryUserMapper;
import com.example.demo.mapper.secondary.SecondaryUserMapper;

@Service
public class UserService {

    @Autowired
    private PrimaryUserMapper primaryUserMapper;

    @Autowired
    private SecondaryUserMapper secondaryUserMapper;

    @Transactional("primaryTransactionManager")
    public void primaryTransactionMethod() {
        // 主数据源的数据库操作
        primaryUserMapper.insertUser();
    }

    @Transactional("secondaryTransactionManager")
    public void secondaryTransactionMethod() {
        // 从数据源的数据库操作
        secondaryUserMapper.insertUser();
    }
}

本地事务管理的优点是实现简单,性能开销小,但它只能保证单个数据源的事务一致性,无法解决多个数据源之间的一致性问题。

9.2.2 分布式事务管理

分布式事务是指涉及多个数据源或多个服务的事务。常见的分布式事务解决方案有两阶段提交(2PC)、三阶段提交(3PC)、TCC(Try-Confirm-Cancel)和消息最终一致性等。

  • 两阶段提交(2PC)
    两阶段提交是一种经典的分布式事务解决方案,它分为准备阶段和提交阶段。在准备阶段,协调者向所有参与者发送准备请求,参与者执行事务操作并将结果反馈给协调者;在提交阶段,协调者根据所有参与者的反馈决定是否提交或回滚事务。
    在 Spring Boot 中,可以使用 JTA(Java Transaction API)和 Atomikos 来实现两阶段提交。
    • 添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
- 配置数据源和事务管理器
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class DistributedTransactionConfig {

    @Bean(name = "distributedDataSource")
    @Primary
    public DataSource distributedDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        // 配置数据源信息
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setUniqueResourceName("distributedDataSource");
        dataSource.setPoolSize(5);
        dataSource.setMinPoolSize(1);
        dataSource.setMaxPoolSize(10);
        dataSource.setBorrowConnectionTimeout(60);
        dataSource.setReapTimeout(20);
        return dataSource;
    }
}
- 使用分布式事务
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.mapper.primary.PrimaryUserMapper;
import com.example.demo.mapper.secondary.SecondaryUserMapper;

@Service
public class DistributedUserService {

    @Autowired
    private PrimaryUserMapper primaryUserMapper;

    @Autowired
    private SecondaryUserMapper secondaryUserMapper;

    @Transactional
    public void distributedTransactionMethod() {
        primaryUserMapper.insertUser();
        secondaryUserMapper.insertUser();
    }
}

两阶段提交的优点是可以保证数据的强一致性,但它存在单点故障、性能开销大等问题。

  • TCC(Try-Confirm-Cancel)
    TCC 是一种补偿型的分布式事务解决方案,它将一个业务操作分为三个阶段:Try 阶段进行资源的预留和检查,Confirm 阶段进行资源的确认和提交,Cancel 阶段进行资源的释放和回滚。
    在 Spring Boot 中,可以使用 Seata 框架来实现 TCC 事务。
    • 添加依赖
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>
- 配置 Seata
seata.enabled=true
seata.tx-service-group=my_test_tx_group
seata.config.type=file
seata.registry.type=file
- 实现 TCC 业务逻辑
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface UserTccService {

    @TwoPhaseBusinessAction(name = "userTccService", commitMethod = "commit", rollbackMethod = "rollback")
    boolean tryUser(@BusinessActionContextParameter(paramName = "userId") Long userId);

    boolean commit(BusinessActionContext actionContext);

    boolean rollback(BusinessActionContext actionContext);
}

TCC 的优点是性能较高,适合对性能要求较高的场景,但它的实现复杂度较高。

9.3 多数据源事务管理的实践案例

以下是一个使用 Spring Boot 和 JTA/Atomikos 实现多数据源事务管理的完整案例:

9.3.1 项目配置

pom.xml 中添加依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    </dependency>
</dependencies>
9.3.2 数据源配置
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
    @Primary
    public DataSource primaryDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setUniqueResourceName("primaryDataSource");
        dataSource.setPoolSize(5);
        dataSource.setMinPoolSize(1);
        dataSource.setMaxPoolSize(10);
        dataSource.setBorrowConnectionTimeout(60);
        dataSource.setReapTimeout(20);
        // 配置主数据源连接信息
        dataSource.setXaProperties(getXaProperties("jdbc:mysql://localhost:3306/primary_db", "root", "root"));
        return dataSource;
    }

    @Bean(name = "secondaryDataSource")
    public DataSource secondaryDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setUniqueResourceName("secondaryDataSource");
        dataSource.setPoolSize(5);
        dataSource.setMinPoolSize(1);
        dataSource.setMaxPoolSize(10);
        dataSource.setBorrowConnectionTimeout(60);
        dataSource.setReapTimeout(20);
        // 配置从数据源连接信息
        dataSource.setXaProperties(getXaProperties("jdbc:mysql://localhost:3306/secondary_db", "root", "root"));
        return dataSource;
    }

    private java.util.Properties getXaProperties(String url, String username, String password) {
        java.util.Properties properties = new java.util.Properties();
        properties.setProperty("URL", url);
        properties.setProperty("user", username);
        properties.setProperty("password", password);
        return properties;
    }
}
9.3.3 事务管理器配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionManagerConfig {

    @Bean(name = "distributedTransactionManager")
    public PlatformTransactionManager distributedTransactionManager(DataSource primaryDataSource, DataSource secondaryDataSource) {
        return new org.springframework.transaction.jta.JtaTransactionManager();
    }
}
9.3.4 业务服务类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.mapper.primary.PrimaryUserMapper;
import com.example.demo.mapper.secondary.SecondaryUserMapper;

@Service
public class UserService {

    @Autowired
    private PrimaryUserMapper primaryUserMapper;

    @Autowired
    private SecondaryUserMapper secondaryUserMapper;

    @Transactional("distributedTransactionManager")
    public void multiDataSourceTransaction() {
        primaryUserMapper.insertUser();
        secondaryUserMapper.insertUser();
    }
}
9.3.5 控制器类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.service.UserService;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/multi-ds-transaction")
    public String multiDataSourceTransaction() {
        try {
            userService.multiDataSourceTransaction();
            return "Transaction success";
        } catch (Exception e) {
            return "Transaction failed: " + e.getMessage();
        }
    }
}

9.4 多数据源事务管理的注意事项

  • 异常处理:在多数据源事务中,要确保对异常进行正确的捕获和处理,以便在出现异常时能够及时回滚事务。
  • 性能优化:分布式事务会带来一定的性能开销,要根据业务场景选择合适的事务管理方案,并进行性能优化。例如,使用 TCC 可以减少锁的持有时间,提高系统的并发性能。
  • 数据一致性:不同的事务管理方案对数据一致性的保证程度不同,要根据业务需求选择合适的方案。例如,对于对数据一致性要求较高的场景,可以选择两阶段提交;对于对性能要求较高的场景,可以选择消息最终一致性。

十、代码示例与项目实战

10.1 项目概述

本项目是一个简单的用户管理系统,旨在通过 Spring Boot 整合 MyBatis 实现基本的用户信息增删改查功能,并拓展到多数据源的使用。系统包含两个数据源,一个主数据源用于存储核心用户信息,另一个从数据源用于存储用户的额外信息,同时会对整个系统进行事务管理,确保数据的一致性。

10.2 项目环境搭建

10.2.1 创建 Spring Boot 项目

使用 Spring Initializr(https://start.spring.io/)创建一个新的 Spring Boot 项目,选择以下依赖:

  • Spring Web
  • MyBatis Framework
  • MySQL Driver
  • Spring Boot Starter Data JPA(可选,用于后续对比参考)
  • Spring Boot Starter Transaction(用于事务管理)
10.2.2 配置数据库连接

application.yml 中配置两个数据源的连接信息:

spring:
  datasource:
    primary:
      url: jdbc:mysql://localhost:3306/primary_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      url: jdbc:mysql://localhost:3306/secondary_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

10.3 实体类设计

10.3.1 用户主信息实体类(User)
package com.example.demo.entity;

import java.io.Serializable;

public class User implements Serializable {
    private Integer id;
    private String name;
    private Integer age;

    public User() {}

    public User(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
10.3.2 用户额外信息实体类(UserExtraInfo)
package com.example.demo.entity;

import java.io.Serializable;

public class UserExtraInfo implements Serializable {
    private Integer id;
    private String email;
    private String phone;

    public UserExtraInfo() {}

    public UserExtraInfo(Integer id, String email, String phone) {
        this.id = id;
        this.email = email;
        this.phone = phone;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "UserExtraInfo{" +
                "id=" + id +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

10.4 Mapper 接口与 XML 文件编写

10.4.1 主数据源 Mapper 接口(UserMapper)
package com.example.demo.mapper.primary;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserMapper {
    List<User> getAllUsers();
    User getUserById(Integer id);
    int insertUser(User user);
    int updateUser(User user);
    int deleteUser(Integer id);
}
10.4.2 主数据源 Mapper XML 文件(UserMapper.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.demo.mapper.primary.UserMapper">
    <select id="getAllUsers" resultType="com.example.demo.entity.User">
        SELECT * FROM user
    </select>
    <select id="getUserById" resultType="com.example.demo.entity.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
    <insert id="insertUser" parameterType="com.example.demo.entity.User">
        INSERT INTO user (name, age) VALUES (#{name}, #{age})
    </insert>
    <update id="updateUser" parameterType="com.example.demo.entity.User">
        UPDATE user SET name = #{name}, age = #{age} WHERE id = #{id}
    </update>
    <delete id="deleteUser" parameterType="java.lang.Integer">
        DELETE FROM user WHERE id = #{id}
    </delete>
</mapper>
10.4.3 从数据源 Mapper 接口(UserExtraInfoMapper)
package com.example.demo.mapper.secondary;

import com.example.demo.entity.UserExtraInfo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface UserExtraInfoMapper {
    List<UserExtraInfo> getAllUserExtraInfos();
    UserExtraInfo getUserExtraInfoById(Integer id);
    int insertUserExtraInfo(UserExtraInfo userExtraInfo);
    int updateUserExtraInfo(UserExtraInfo userExtraInfo);
    int deleteUserExtraInfo(Integer id);
}
10.4.4 从数据源 Mapper XML 文件(UserExtraInfoMapper.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.demo.mapper.secondary.UserExtraInfoMapper">
    <select id="getAllUserExtraInfos" resultType="com.example.demo.entity.UserExtraInfo">
        SELECT * FROM user_extra_info
    </select>
    <select id="getUserExtraInfoById" resultType="com.example.demo.entity.UserExtraInfo">
        SELECT * FROM user_extra_info WHERE id = #{id}
    </select>
    <insert id="insertUserExtraInfo" parameterType="com.example.demo.entity.UserExtraInfo">
        INSERT INTO user_extra_info (email, phone) VALUES (#{email}, #{phone})
    </insert>
    <update id="updateUserExtraInfo" parameterType="com.example.demo.entity.UserExtraInfo">
        UPDATE user_extra_info SET email = #{email}, phone = #{phone} WHERE id = #{id}
    </update>
    <delete id="deleteUserExtraInfo" parameterType="java.lang.Integer">
        DELETE FROM user_extra_info WHERE id = #{id}
    </delete>
</mapper>

10.5 服务层(Service)实现

10.5.1 主数据源服务类(UserService)
package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.primary.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public List<User> getAllUsers() {
        return userMapper.getAllUsers();
    }

    public User getUserById(Integer id) {
        return userMapper.getUserById(id);
    }

    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

    public int updateUser(User user) {
        return userMapper.updateUser(user);
    }

    public int deleteUser(Integer id) {
        return userMapper.deleteUser(id);
    }
}
10.5.2 从数据源服务类(UserExtraInfoService)
package com.example.demo.service;

import com.example.demo.entity.UserExtraInfo;
import com.example.demo.mapper.secondary.UserExtraInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserExtraInfoService {
    @Autowired
    private UserExtraInfoMapper userExtraInfoMapper;

    public List<UserExtraInfo> getAllUserExtraInfos() {
        return userExtraInfoMapper.getAllUserExtraInfos();
    }

    public UserExtraInfo getUserExtraInfoById(Integer id) {
        return userExtraInfoMapper.getUserExtraInfoById(id);
    }

    public int insertUserExtraInfo(UserExtraInfo userExtraInfo) {
        return userExtraInfoMapper.insertUserExtraInfo(userExtraInfo);
    }

    public int updateUserExtraInfo(UserExtraInfo userExtraInfo) {
        return userExtraInfoMapper.updateUserExtraInfo(userExtraInfo);
    }

    public int deleteUserExtraInfo(Integer id) {
        return userExtraInfoMapper.deleteUserExtraInfo(id);
    }
}

10.6 控制器层(Controller)实现

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.entity.UserExtraInfo;
import com.example.demo.service.UserExtraInfoService;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private UserExtraInfoService userExtraInfoService;

    @GetMapping
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Integer id) {
        return userService.getUserById(id);
    }

    @PostMapping
    public int insertUser(@RequestBody User user) {
        return userService.insertUser(user);
    }

    @PutMapping
    public int updateUser(@RequestBody User user) {
        return userService.updateUser(user);
    }

    @DeleteMapping("/{id}")
    public int deleteUser(@PathVariable Integer id) {
        return userService.deleteUser(id);
    }

    @GetMapping("/extra")
    public List<UserExtraInfo> getAllUserExtraInfos() {
        return userExtraInfoService.getAllUserExtraInfos();
    }

    @GetMapping("/extra/{id}")
    public UserExtraInfo getUserExtraInfoById(@PathVariable Integer id) {
        return userExtraInfoService.getUserExtraInfoById(id);
    }

    @PostMapping("/extra")
    public int insertUserExtraInfo(@RequestBody UserExtraInfo userExtraInfo) {
        return userExtraInfoService.insertUserExtraInfo(userExtraInfo);
    }

    @PutMapping("/extra")
    public int updateUserExtraInfo(@RequestBody UserExtraInfo userExtraInfo) {
        return userExtraInfoService.updateUserExtraInfo(userExtraInfo);
    }

    @DeleteMapping("/extra/{id}")
    public int deleteUserExtraInfo(@PathVariable Integer id) {
        return userExtraInfoService.deleteUserExtraInfo(id);
    }
}

10.7 多数据源事务管理实现

10.7.1 数据源配置类
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean(name = "secondaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean(name = "primaryTransactionManager")
    public PlatformTransactionManager primaryTransactionManager(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        return new DataSourceTransactionManager(primaryDataSource);
    }

    @Bean(name = "secondaryTransactionManager")
    public PlatformTransactionManager secondaryTransactionManager(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new DataSourceTransactionManager(secondaryDataSource);
    }
}
10.7.2 事务服务类
package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.entity.UserExtraInfo;
import com.example.demo.mapper.primary.UserMapper;
import com.example.demo.mapper.secondary.UserExtraInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionalUserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private UserExtraInfoMapper userExtraInfoMapper;

    @Transactional("primaryTransactionManager")
    public void insertUserTransactional(User user) {
        userMapper.insertUser(user);
    }

    @Transactional("secondaryTransactionManager")
    public void insertUserExtraInfoTransactional(UserExtraInfo userExtraInfo) {
        userExtraInfoMapper.insertUserExtraInfo(userExtraInfo);
    }

    @Transactional(transactionManager = "primaryTransactionManager", rollbackFor = Exception.class)
    public void complexTransactionalOperation(User user, UserExtraInfo userExtraInfo) {
        try {
            userMapper.insertUser(user);
            userExtraInfoMapper.insertUserExtraInfo(userExtraInfo);
        } catch (Exception e) {
            throw new RuntimeException("Transaction failed", e);
        }
    }
}

10.8 项目测试

10.8.1 启动项目

运行 Spring Boot 项目的主类,确保项目正常启动。

10.8.2 测试接口

使用 Postman 或其他 API 测试工具,对控制器层的接口进行测试:

  • 测试获取所有用户信息:发送 GET 请求到 http://localhost:8080/users
  • 测试根据 ID 获取用户信息:发送 GET 请求到 http://localhost:8080/users/1
  • 测试插入用户信息:发送 POST 请求到 http://localhost:8080/users,并在请求体中添加用户信息的 JSON 数据
  • 测试更新用户信息:发送 PUT 请求到 http://localhost:8080/users,并在请求体中添加更新后的用户信息的 JSON 数据
  • 测试删除用户信息:发送 DELETE 请求到 http://localhost:8080/users/1

同时,对从数据源的接口也进行类似的测试,确保多数据源的操作都能正常执行。

10.8.3 测试事务管理

调用 TransactionalUserService 中的 complexTransactionalOperation 方法,在方法中插入用户主信息和额外信息,模拟一个需要事务管理的复杂操作。可以通过故意抛出异常来测试事务的回滚功能,确保数据的一致性。

十一、常见问题与解决方案

11.1 配置相关问题

11.1.1 数据源配置失败
  • 问题描述:在配置数据源时,可能会出现无法连接数据库的情况,例如在启动 Spring Boot 项目时,控制台抛出 java.sql.SQLException 异常,提示无法建立数据库连接。
  • 可能原因
    • 数据库地址、端口、用户名、密码等配置信息错误。
    • 数据库服务未启动。
    • 数据库驱动依赖未正确添加。
  • 解决方案
    • 检查 application.propertiesapplication.yml 中的数据源配置信息,确保地址、端口、用户名和密码正确。例如,在 application.yml 中检查如下配置:
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_database_name
    username: your_username
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver
- 检查数据库服务是否正常启动。可以通过命令行或数据库管理工具(如 MySQL Workbench)来验证。
- 确保在 `pom.xml` 中添加了正确的数据库驱动依赖。以 MySQL 为例:
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.26</version>
</dependency>
11.1.2 MyBatis 配置文件加载失败
  • 问题描述:Mapper XML 文件或配置文件无法被正确加载,导致 SQL 语句无法执行,可能会抛出 org.apache.ibatis.binding.BindingException 异常。
  • 可能原因
    • mybatis.mapper-locationsmybatis.config-location 配置错误。
    • Mapper XML 文件的路径或命名不符合配置要求。
  • 解决方案
    • 检查 application.propertiesapplication.yml 中 MyBatis 配置文件的路径配置。例如,在 application.yml 中:
mybatis:
  mapper-locations: classpath:mapper/*.xml
  config-location: classpath:mybatis-config.xml
- 确保 Mapper XML 文件的路径和命名与配置一致。例如,若配置为 `classpath:mapper/*.xml`,则 Mapper XML 文件应放在 `src/main/resources/mapper` 目录下。

11.2 SQL 执行问题

11.2.1 SQL 语法错误
  • 问题描述:在执行 SQL 语句时,抛出 java.sql.SQLSyntaxErrorException 异常,提示 SQL 语法错误。
  • 可能原因
    • SQL 语句编写错误,例如关键字拼写错误、表名或列名错误、语法结构错误等。
    • 不同数据库的 SQL 语法存在差异,未使用对应数据库支持的语法。
  • 解决方案
    • 仔细检查 SQL 语句,确保关键字、表名、列名等拼写正确,语法结构符合 SQL 规范。可以在数据库管理工具中先测试 SQL 语句的正确性。
    • 了解所使用数据库的 SQL 语法特点,避免使用不支持的语法。例如,MySQL 和 Oracle 在日期函数、字符串拼接等方面存在差异。
11.2.2 SQL 执行超时
  • 问题描述:SQL 语句执行时间过长,超过了预设的超时时间,导致查询或操作失败。
  • 可能原因
    • 数据库表数据量过大,查询语句未使用合适的索引,导致全表扫描。
    • 数据库服务器性能不足,如 CPU、内存、磁盘 I/O 等资源瓶颈。
    • SQL 语句本身存在性能问题,如嵌套子查询过多、使用了复杂的连接操作等。
  • 解决方案
    • 对经常用于查询条件的字段创建索引,以提高查询效率。例如,在 MySQL 中可以使用 CREATE INDEX 语句创建索引:
CREATE INDEX idx_name ON user (name);
- 优化数据库服务器的性能,如增加硬件资源、调整数据库参数等。
- 优化 SQL 语句,避免复杂的嵌套子查询和连接操作。可以将复杂的查询拆分成多个简单的查询,通过 Java 代码进行逻辑处理。

11.3 事务管理问题

11.3.1 事务不生效
  • 问题描述:在使用 @Transactional 注解进行事务管理时,事务没有按照预期回滚,即使方法中抛出异常,数据也没有回滚。
  • 可能原因
    • @Transactional 注解使用位置错误,例如在非公共方法上使用该注解。
    • 异常类型未被正确捕获和处理,导致事务无法感知异常。
    • 事务管理器配置错误。
  • 解决方案
    • 确保 @Transactional 注解应用在公共方法上,因为 Spring AOP 只对公共方法进行代理。
    • 检查 @Transactional 注解的 rollbackFor 属性,确保指定了需要回滚的异常类型。例如:
@Transactional(rollbackFor = Exception.class)
public void someMethod() {
    // 业务逻辑
}
- 检查事务管理器的配置,确保其正确配置并与数据源关联。例如,在配置类中正确配置事务管理器:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;

@Configuration
public class TransactionManagerConfig {
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
11.3.2 多数据源事务不一致
  • 问题描述:在多数据源环境下,进行跨数据源的事务操作时,可能会出现数据不一致的情况,例如一个数据源的操作成功,另一个数据源的操作失败,但没有实现统一的回滚。
  • 可能原因
    • 没有使用合适的分布式事务管理方案,本地事务管理器无法处理跨数据源的事务。
    • 分布式事务协调器配置错误或出现故障。
  • 解决方案
    • 选择合适的分布式事务管理方案,如两阶段提交(2PC)、TCC(Try-Confirm-Cancel)或消息最终一致性等。可以使用开源框架如 Seata 来实现分布式事务管理。
    • 检查分布式事务协调器的配置,确保其正常工作。例如,在使用 Seata 时,需要正确配置 Seata 服务器和客户端。

11.4 性能问题

11.4.1 系统响应缓慢
  • 问题描述:系统处理请求的响应时间过长,用户体验不佳。
  • 可能原因
    • 数据库查询性能低下,如未使用索引、SQL 语句复杂等。
    • 代码中存在性能瓶颈,如循环嵌套过多、频繁的数据库操作等。
    • 服务器硬件资源不足,如 CPU、内存、网络带宽等。
  • 解决方案
    • 对数据库进行性能优化,如创建索引、优化 SQL 语句等。
    • 对代码进行性能分析,找出性能瓶颈并进行优化。可以使用工具如 VisualVM、YourKit 等进行性能分析。
    • 评估服务器硬件资源,根据实际情况进行升级或优化。
11.4.2 内存溢出
  • 问题描述:系统在运行过程中出现 OutOfMemoryError 异常,提示内存溢出。
  • 可能原因
    • 代码中存在内存泄漏问题,如对象未及时释放、缓存数据过多等。
    • 一次性加载大量数据到内存中,导致内存占用过高。
  • 解决方案
    • 检查代码,确保对象在不再使用时及时释放。例如,在使用完数据库连接、文件流等资源后,及时关闭。
    • 优化数据加载方式,避免一次性加载大量数据到内存中。可以采用分页查询、分批处理等方式。

十二、总结与展望

12.1 总结

12.1.1 Spring Boot 与 MyBatis 整合要点回顾

在本指南中,我们详细探讨了 Spring Boot 与 MyBatis 的整合过程。从基础配置开始,我们学会了如何在 Spring Boot 项目中引入 MyBatis 依赖,并进行数据源的配置。通过配置文件或 Java 代码的方式,我们可以轻松地连接到数据库,为后续的数据操作奠定基础。

在 MyBatis 的使用方面,我们了解了常用注解(如 @Select@Insert@Update@Delete 等)和 XML 映射文件的使用方法。注解方式简洁直观,适合简单的 SQL 操作;而 XML 映射文件则更适合复杂的 SQL 语句和动态 SQL 的编写,能够提高代码的可维护性。

多表查询与关联映射是数据库操作中的重要部分,我们通过实际案例展示了如何处理一对一、一对多和多对多的关联关系,以及如何使用 MyBatis 实现高效的多表查询。动态 SQL 的使用让我们能够根据不同的条件灵活构建 SQL 语句,提高了代码的复用性和灵活性。

分页查询是处理大量数据时的常用技术,我们介绍了使用 LIMIT 关键字和 MyBatis 分页插件(如 PageHelper)实现分页查询的方法,并对分页查询的性能调优进行了深入探讨,包括索引优化、避免大偏移量查询和缓存优化等。

12.1.2 多数据源配置与事务管理总结

多数据源配置在实际项目中具有重要意义,我们详细介绍了手动配置多数据源和使用 AOP 动态切换数据源的方法。通过配置多个数据源和对应的 SqlSessionFactory,我们可以实现对不同数据库的操作。同时,使用 AOP 可以根据业务需求动态切换数据源,提高了系统的灵活性和可扩展性。

多数据源事务管理是一个复杂的问题,我们介绍了本地事务管理和分布式事务管理的方案。本地事务管理适用于单个数据源的事务处理,实现简单但无法保证多数据源之间的一致性;分布式事务管理则可以解决多数据源之间的数据一致性问题,如两阶段提交(2PC)、TCC(Try-Confirm-Cancel)等方案,但实现复杂度较高。

12.2 展望

12.2.1 技术发展趋势

随着云计算、大数据和人工智能等技术的不断发展,数据库技术也在不断演进。未来,数据库将朝着分布式、高并发、高性能和智能化的方向发展。Spring Boot 与 MyBatis 的整合也将面临新的挑战和机遇。

一方面,分布式数据库的广泛应用将要求我们更加深入地研究多数据源和分布式事务管理技术,以确保数据的一致性和高可用性。例如,使用分布式数据库中间件和分布式事务协调器来实现跨多个数据库节点的事务处理。

另一方面,人工智能和机器学习技术的应用将为数据库操作带来新的思路。例如,通过机器学习算法优化 SQL 查询计划,提高查询性能;利用人工智能技术进行数据挖掘和分析,为业务决策提供支持。

12.2.2 未来应用拓展

在实际应用中,Spring Boot 与 MyBatis 的整合可以应用于更多的领域和场景。例如,在微服务架构中,每个微服务可以使用独立的数据库,通过 Spring Boot 和 MyBatis 实现对各自数据库的操作,同时使用分布式事务管理来保证微服务之间的数据一致性。

在物联网领域,大量的设备会产生海量的数据,需要高效的数据库管理和查询。Spring Boot 与 MyBatis 可以用于构建物联网数据管理平台,实现对设备数据的存储、查询和分析。

此外,随着区块链技术的发展,Spring Boot 与 MyBatis 也可以与区块链技术相结合,构建分布式账本系统,实现数据的不可篡改和安全共享。

12.3 给开发者的建议

12.3.1 持续学习与实践

Spring Boot 和 MyBatis 是不断发展和更新的技术,开发者需要持续学习新的特性和功能,关注官方文档和社区动态。同时,要通过实践不断积累经验,将所学知识应用到实际项目中,提高解决问题的能力。

12.3.2 代码规范与性能优化

在开发过程中,要遵循良好的代码规范,提高代码的可读性和可维护性。同时,要注重性能优化,合理使用索引、缓存等技术,避免出现性能瓶颈。例如,在编写 SQL 语句时,要避免使用 SELECT *,尽量只查询需要的字段。

12.3.3 关注安全问题

数据库安全是项目开发中不可忽视的问题,开发者要关注数据的安全性,避免 SQL 注入、数据泄露等安全漏洞。例如,使用预编译语句来防止 SQL 注入,对敏感数据进行加密存储等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fanxbl957

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值