MySQL存储过程之:`存储过程`与`ORM`的`混合`使用:其在业务逻辑分层中的应用。

好的,下面开始本次讲座:

MySQL存储过程与ORM的混合使用:业务逻辑分层中的应用

大家好,今天我们要探讨的是MySQL存储过程与ORM(对象关系映射)在业务逻辑分层中的混合使用,以及它们如何协同工作以构建更高效、更可维护的应用程序。

一、为什么需要混合使用存储过程和ORM?

在传统的Web应用架构中,我们通常会将应用程序分为三层:

  • 表示层(Presentation Layer): 负责用户交互,例如前端页面和API接口。
  • 业务逻辑层(Business Logic Layer): 处理应用程序的核心业务规则和流程。
  • 数据访问层(Data Access Layer): 负责与数据库进行交互,包括数据的读取、写入、更新和删除。

ORM 框架(例如Hibernate、MyBatis、Entity Framework等)主要用于数据访问层,简化了与数据库的交互。ORM将数据库表映射到对象,允许开发者使用面向对象的方式操作数据,避免了编写大量的SQL语句。

存储过程则是在数据库服务器端预编译并存储的SQL代码块,它可以接受参数、执行复杂的逻辑、返回结果集。

那么,为什么我们需要将ORM和存储过程混合使用,而不是只选择其中一种方案呢?

特性 ORM 存储过程
优点 开发效率高,面向对象编程,易于维护,可移植性好 性能高,安全性好,减少网络传输,事务控制灵活
缺点 性能损耗,复杂SQL优化困难,安全性相对较弱 开发效率较低,调试困难,可移植性差

简单来说,ORM擅长处理简单的数据CRUD操作,提升开发效率。而存储过程则更适合处理复杂的业务逻辑,优化性能,并提供更高的安全性。

二、混合使用的策略

混合使用存储过程和ORM的关键在于明确它们各自的职责,以及如何进行协作。以下是一些常见的策略:

  1. ORM负责基础CRUD操作: 对于简单的增删改查操作,使用ORM能够快速完成开发,并保持代码的可读性和可维护性。
  2. 存储过程处理复杂业务逻辑: 将复杂的业务逻辑封装到存储过程中,例如涉及多个表的关联查询、复杂的计算、数据校验等。
  3. ORM调用存储过程: 通过ORM框架调用存储过程,将存储过程的执行结果映射到对象。
  4. 事务控制: 事务控制可以在ORM层或存储过程层进行。如果在存储过程中进行事务控制,需要确保ORM框架的事务管理机制与之兼容。

三、具体实现示例

接下来,我们通过一个示例来演示如何在实际项目中混合使用存储过程和ORM。假设我们有一个电商系统,需要实现一个“创建订单”的功能。该功能涉及到多个表的操作,包括订单表、订单详情表、商品表等。

1. 数据库表结构

CREATE TABLE `orders` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` INT UNSIGNED NOT NULL,
  `order_number` VARCHAR(50) NOT NULL,
  `total_amount` DECIMAL(10,2) NOT NULL,
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `order_details` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `order_id` INT UNSIGNED NOT NULL,
  `product_id` INT UNSIGNED NOT NULL,
  `quantity` INT UNSIGNED NOT NULL,
  `price` DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `products` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(100) NOT NULL,
  `price` DECIMAL(10,2) NOT NULL,
  `stock` INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

2. ORM实体类(以Java为例,使用MyBatis)

public class Order {
    private Integer id;
    private Integer userId;
    private String orderNumber;
    private BigDecimal totalAmount;
    private Date createTime;

    // Getters and Setters
}

public class OrderDetail {
    private Integer id;
    private Integer orderId;
    private Integer productId;
    private Integer quantity;
    private BigDecimal price;

    // Getters and Setters
}

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

    // Getters and Setters
}

3. MyBatis Mapper接口

public interface OrderMapper {
    @Select("SELECT * FROM orders WHERE id = #{id}")
    Order selectOrderById(Integer id);

    @Insert("INSERT INTO orders (user_id, order_number, total_amount) VALUES (#{userId}, #{orderNumber}, #{totalAmount})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insertOrder(Order order);

    @Insert("INSERT INTO order_details (order_id, product_id, quantity, price) VALUES (#{orderId}, #{productId}, #{quantity}, #{price})")
    int insertOrderDetail(OrderDetail orderDetail);
}

4. 存储过程

以下存储过程用于创建订单,同时更新商品库存,并处理事务:

DELIMITER //

CREATE PROCEDURE `create_order`(
    IN `user_id_param` INT UNSIGNED,
    IN `product_ids_param` VARCHAR(255), -- 逗号分隔的商品ID
    IN `quantities_param` VARCHAR(255), -- 逗号分隔的商品数量
    OUT `order_id_out` INT UNSIGNED,
    OUT `error_code_out` INT -- 0:成功, 1:库存不足, 2:其他错误
)
BEGIN
    DECLARE `order_number_var` VARCHAR(50);
    DECLARE `total_amount_var` DECIMAL(10,2) DEFAULT 0;
    DECLARE `product_id_var` INT UNSIGNED;
    DECLARE `quantity_var` INT UNSIGNED;
    DECLARE `i` INT UNSIGNED DEFAULT 1;
    DECLARE `product_count` INT UNSIGNED;
    DECLARE `product_ids_arr` TEXT;
    DECLARE `quantities_arr` TEXT;
    DECLARE `product_price` DECIMAL(10,2);
    DECLARE `product_stock` INT UNSIGNED;

    SET `error_code_out` = 0; -- 默认成功

    -- 创建临时表存储分割后的商品ID和数量
    CREATE TEMPORARY TABLE IF NOT EXISTS `temp_order_items` (
        `product_id` INT UNSIGNED NOT NULL,
        `quantity` INT UNSIGNED NOT NULL
    );

    -- 生成订单号
    SET `order_number_var` = DATE_FORMAT(NOW(), '%Y%m%d%H%i%s');

    -- 将逗号分隔的字符串转换为临时表
    SET `product_ids_arr` = `product_ids_param`;
    SET `quantities_arr` = `quantities_param`;
    SET `product_count` = (LENGTH(`product_ids_arr`) - LENGTH(REPLACE(`product_ids_arr`, ',', '')) + 1);

    -- 循环处理每个商品
    WHILE `i` <= `product_count` DO
        -- 获取商品ID
        SET `product_id_var` = SUBSTRING_INDEX(SUBSTRING_INDEX(`product_ids_arr`, ',', `i`), ',', -1);

        -- 获取商品数量
        SET `quantity_var` = SUBSTRING_INDEX(SUBSTRING_INDEX(`quantities_arr`, ',', `i`), ',', -1);

        -- 插入到临时表
        INSERT INTO `temp_order_items` (`product_id`, `quantity`) VALUES (`product_id_var`, `quantity_var`);

        SET `i` = `i` + 1;
    END WHILE;

    -- 开启事务
    START TRANSACTION;

    -- 循环处理临时表中的商品
    SET `i` = 1;
    WHILE `i` <= `product_count` DO
        SELECT `product_id`, `quantity` INTO `product_id_var`, `quantity_var` FROM `temp_order_items` LIMIT `i` - 1, 1;

        -- 获取商品价格和库存
        SELECT `price`, `stock` INTO `product_price`, `product_stock` FROM `products` WHERE `id` = `product_id_var` FOR UPDATE; -- 加行级锁

        -- 检查库存是否足够
        IF `product_stock` < `quantity_var` THEN
            SET `error_code_out` = 1; -- 库存不足
            ROLLBACK;
            DROP TEMPORARY TABLE IF EXISTS `temp_order_items`;
            SELECT '库存不足' AS 'message';
            LEAVE `create_order`;
        END IF;

        -- 更新商品库存
        UPDATE `products` SET `stock` = `stock` - `quantity_var` WHERE `id` = `product_id_var`;

        -- 计算订单总金额
        SET `total_amount_var` = `total_amount_var` + (`product_price` * `quantity_var`);

        SET `i` = `i` + 1;
    END WHILE;

    -- 创建订单
    INSERT INTO `orders` (`user_id`, `order_number`, `total_amount`) VALUES (`user_id_param`, `order_number_var`, `total_amount_var`);
    SET `order_id_out` = LAST_INSERT_ID();

    -- 创建订单详情
    SET `i` = 1;
    WHILE `i` <= `product_count` DO
        SELECT `product_id`, `quantity` INTO `product_id_var`, `quantity_var` FROM `temp_order_items` LIMIT `i` - 1, 1;
        SELECT `price` INTO `product_price` FROM `products` WHERE `id` = `product_id_var`;

        INSERT INTO `order_details` (`order_id`, `product_id`, `quantity`, `price`) VALUES (`order_id_out`, `product_id_var`, `quantity_var`, `product_price`);
        SET `i` = `i` + 1;
    END WHILE;

    -- 提交事务
    COMMIT;

    DROP TEMPORARY TABLE IF EXISTS `temp_order_items`;

END //

DELIMITER ;

5. Java Service层代码

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate; // Spring JDBC

    public Order createOrder(Integer userId, List<Integer> productIds, List<Integer> quantities) {
        // 将商品ID和数量转换为逗号分隔的字符串
        String productIdsStr = String.join(",", productIds.stream().map(String::valueOf).collect(Collectors.toList()));
        String quantitiesStr = String.join(",", quantities.stream().map(String::valueOf).collect(Collectors.toList()));

        // 调用存储过程
        Map<String, Object> params = new HashMap<>();
        params.put("user_id_param", userId);
        params.put("product_ids_param", productIdsStr);
        params.put("quantities_param", quantitiesStr);
        params.put("order_id_out", Types.INTEGER);
        params.put("error_code_out", Types.INTEGER);

        SimpleJdbcCall jdbcCall = new SimpleJdbcCall(jdbcTemplate.getJdbcTemplate())
                .withProcedureName("create_order")
                .declareParameters(
                        new SqlParameter("user_id_param", Types.INTEGER),
                        new SqlParameter("product_ids_param", Types.VARCHAR),
                        new SqlParameter("quantities_param", Types.VARCHAR),
                        new SqlOutParameter("order_id_out", Types.INTEGER),
                        new SqlOutParameter("error_code_out", Types.INTEGER)
                );

        Map<String, Object> result = jdbcCall.execute(params);

        Integer orderId = (Integer) result.get("order_id_out");
        Integer errorCode = (Integer) result.get("error_code_out");

        if (errorCode != 0) {
            // 处理错误
            if (errorCode == 1) {
                throw new RuntimeException("库存不足");
            } else {
                throw new RuntimeException("创建订单失败");
            }
        }

        // 使用ORM查询订单信息
        Order order = orderMapper.selectOrderById(orderId);
        return order;
    }
}

四、代码解释

  1. 数据库表结构: 定义了ordersorder_detailsproducts三张表,分别用于存储订单信息、订单详情和商品信息。
  2. ORM实体类: 定义了与数据库表对应的Java实体类,用于封装数据。
  3. MyBatis Mapper接口: 定义了用于操作ordersorder_details表的Mapper接口,包括查询订单、插入订单和插入订单详情等方法。
  4. 存储过程: 存储过程create_order接受用户ID、商品ID列表和商品数量列表作为输入参数,执行以下操作:
    • 生成订单号。
    • 遍历商品列表,检查库存是否足够。
    • 更新商品库存。
    • 创建订单。
    • 创建订单详情。
    • 返回订单ID和错误码。
    • 使用事务保证数据一致性
    • 使用临时表存储分割后的商品ID和数量,简化了对多个商品的循环处理。
  5. Java Service层代码: OrderService类的createOrder方法负责调用存储过程,并将存储过程的执行结果映射到Order对象。

五、业务逻辑分层体现

在这个示例中,我们可以看到业务逻辑分层的清晰体现:

  • 表示层: (未展示) 接收用户请求,将请求参数传递给业务逻辑层。
  • 业务逻辑层: OrderService类负责处理创建订单的业务逻辑,包括参数校验、调用存储过程、处理错误等。
  • 数据访问层:
    • ORM(MyBatis)负责简单的订单查询操作。
    • 存储过程负责复杂的订单创建、库存更新等操作。

六、混合使用的优势

通过混合使用存储过程和ORM,我们可以获得以下优势:

  • 提高性能: 存储过程在数据库服务器端执行,减少了网络传输,提高了性能。
  • 增强安全性: 存储过程可以控制对数据库的访问权限,防止SQL注入等安全问题。
  • 简化业务逻辑: 将复杂的业务逻辑封装到存储过程中,使业务逻辑层更加简洁。
  • 提高开发效率: 使用ORM处理简单的CRUD操作,提高开发效率。
  • 更好的事务控制: 存储过程可以更灵活的控制事务,确保数据一致性。

七、需要注意的点

  • 可移植性: 存储过程与特定的数据库系统绑定,降低了应用程序的可移植性。因此,在设计存储过程时,需要考虑未来的可移植性需求。
  • 调试: 存储过程的调试相对困难,需要使用数据库提供的调试工具。
  • 版本控制: 存储过程也需要进行版本控制,可以使用数据库提供的版本控制工具,或者将存储过程的SQL脚本纳入代码仓库。
  • 参数传递: 在ORM和存储过程之间传递参数时,需要注意数据类型和格式的兼容性。
  • 复杂性: 存储过程编写和维护难度相对较高,需要权衡其带来的性能提升和复杂性增加。

八、案例:电商平台商品推荐

假设一个电商平台需要实现一个“商品推荐”功能,根据用户的浏览历史和购买记录,推荐相关的商品。

在这个场景中,可以考虑使用存储过程进行复杂的推荐算法计算,因为推荐算法可能涉及到大量的数据分析和计算,使用存储过程可以提高性能。同时,使用ORM来读取用户浏览历史和购买记录,以及将推荐结果映射到商品对象。

九、案例:银行转账业务

银行转账业务的安全性至关重要。可以使用存储过程来执行转账操作,包括账户余额检查、账户余额更新、记录交易日志等。存储过程可以保证转账操作的原子性,避免出现数据不一致的情况。ORM可以用于查询账户信息和交易记录。

总结

将复杂的业务逻辑放在存储过程中,可以提高性能和安全性。使用存储过程处理复杂业务逻辑,使用ORM处理简单CRUD操作,可以提高开发效率和代码可维护性。在ORM层调用存储过程,并处理存储过程的返回结果,确保数据类型兼容。

存储过程和ORM如何更好地协同?

在ORM中配置存储过程调用,将存储过程的结果映射到实体类。使用参数化查询,避免SQL注入攻击。在存储过程中处理事务,保证数据一致性。

选择哪种方案取决于具体情况

对于简单的CRUD操作,使用ORM框架可以提高开发效率。对于复杂的业务逻辑,使用存储过程可以提高性能和安全性。

发表回复

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