返回

CodeIgniter批量插入动态列数据:从表单到数据库

mysql

CodeIgniter 中处理动态多组输入并批量插入数据库

咱们在用 CodeIgniter 开发的时候,经常会遇到处理表单提交过来的多组数据,比如动态添加的行项目。更复杂点的情况是,这些数据的字段名(对应数据库列名)还不是固定的,需要根据某个模板 ID 来确定。这就要了命了,怎么把这些动态列名、多行数据塞进数据库呢?

别急,这事儿不难。看看下面这个场景是不是你遇到的:

你有个表单,用户可以根据选的模板 (id_template) 动态添加好多行数据。假设模板 A 需要填 minmax 两列,模板 B 需要填 pricequantity 两列。提交后,你的 Controller 大概能拿到类似这样的数据结构(通过 $this->input->post()):

  • 如果选了模板 A:
    • min: [10, 20, 30]
    • max: [100, 200, 300]
  • 如果选了模板 B:
    • price: [9.9, 19.9, 29.9]
    • quantity: [5, 10, 15]

同时,你还能根据 id_template 从数据库或者配置文件里查到当前模板应该包含哪些列名,比如 ['min', 'max'] 或者 ['price', 'quantity']

目标很明确:把这些数据,一行一行地,插入到对应模板的数据表里。

问题分析:为啥之前的代码可能不灵

看看文章开头提到的那段代码:

    $column = $this->m_rate_template->get_column($id_template); // 拿到列名数组 ['min', 'max']
    $colum_detail = implode(",", $column); // 变成 "min,max" 字符串
    $column_cut = explode(",", $colum_detail); // 又变回数组 ['min', 'max']
                                             // (其实前两步有点多余,如果 $column 本身就是数组)

    foreach ($column_cut as $key => $val){ // 外层循环:先处理 'min',再处理 'max'

        $a = $this->input->post($column_cut[$key]); // 拿到 'min' 对应的数组 [10, 20, 30]
                                                   // 或者 'max' 对应的数组 [100, 200, 300]

            foreach ($a as $key1 => $val1){ // 内层循环:遍历 'min' 数组的值 (10, 20, 30)
                                           // 或者 'max' 数组的值 (100, 200, 300)
                echo $val1;
                $child_data = array(
                        'id' => $this->m_rate_template->generate_id_in_template($template_name),
                        'id_rate' => $id_rate,
                        $column_cut[$key] => $val1 // 关键:每次只设置了 *一个* 动态列的值
                );
                $this->m_rate_template->insert_rate($child_data, $template_name); // 在内层循环里就插入了
            }
    }

这段代码的问题出在哪?

  1. 循环逻辑不对: 代码是按 来循环的。它先把所有 min 的值,每次一个,构造成一条记录(只有 min 列有值)插入;然后再把所有 max 的值,每次一个,构造成另一条记录(只有 max 列有值)插入。这显然不是我们想要的按 插入(希望的是 {min: 10, max: 100} 一行,{min: 20, max: 200} 一行...)。
  2. 效率低下: 内层循环每遍历一个值就执行一次数据库插入操作。如果提交了 3 行数据,每行 2 列,这就导致了 3 * 2 = 6 次数据库插入,而不是理想中的 3 次。数据量大的时候,性能会很差。
  3. ID 生成问题: generate_id_in_template 在每次内层循环都调用,可能会为本应属于同一逻辑行(比如 min: 10max: 100)的不同片段生成不同的 ID,或者造成不必要的 ID 生成开销。id_rate 同样面临被重复用于不同插入片段的问题。

明白了问题所在,咱们就能对症下药了。

解决方案

核心思路是调整数据结构和循环方式,先按 组织好数据,再一次性或者逐行插入。

方案一:重构数据,逐行插入

这个方法比较直观,先根据提交的数据重新组织成一行一行的数据结构,然后循环这个行数组,每次插入一条完整的记录。

原理和作用:

  1. 先获取所有动态列名。
  2. 获取每个动态列对应的输入数组。
  3. 确定有多少行数据需要插入(通常可以取第一个输入数组的长度)。
  4. 循环这个行数,在每次循环里,根据当前的行索引,从所有输入数组中取出对应位置的值,组合成一个包含所有动态列和固定列(如 id, id_rate)的关联数组。
  5. 调用模型的方法,插入这一行数据。

代码示例(Controller):

<?php defined('BASEPATH') OR exit('No direct script access allowed');

class Your_controller extends CI_Controller {

    public function __construct() {
        parent::__construct();
        // 载入必要的模型,这里假设模型名为 M_rate_template
        $this->load->model('m_rate_template');
        // 载入表单辅助函数(如果需要用到CI的表单验证)和数据库类
        $this->load->helper('form');
        $this->load->database();
    }

    public function process_form() {
        // 假设 id_template 和 id_rate 是通过 POST 或其他方式获取的
        $id_template = $this->input->post('id_template');
        $id_rate = $this->input->post('id_rate'); // 假设这个是固定的关联ID
        $template_name = $this->m_rate_template->get_template_table_name($id_template); // 获取模板对应的表名

        if (empty($id_template) || empty($template_name)) {
            // 处理错误:模板ID无效或找不到对应表名
            show_error('无效的模板信息。');
            return;
        }

        // 1. 获取动态列名 (假设模型方法返回的就是数组)
        $dynamic_columns = $this->m_rate_template->get_column($id_template); // 例如: ['min', 'max']

        if (empty($dynamic_columns)) {
            // 处理错误:模板没有配置列
             show_error('模板未配置有效列。');
            return;
        }

        // 2. 获取所有动态列的输入数组,并做初步检查
        $input_data_arrays = [];
        $row_count = 0;
        $is_first_column = true;

        foreach ($dynamic_columns as $column_name) {
            $post_data = $this->input->post($column_name); // 获取名为 'min' 或 'max' 的 post 数组

            // 安全检查:确保获取到的是数组
            if (!is_array($post_data)) {
                log_message('error', '表单字段 ' . $column_name . ' 未提交或格式不正确 (期望数组)。');
                // 可以根据业务逻辑选择报错、跳过或赋默认值
                 show_error('提交的数据格式不正确,请检查表单。');
                 return; // 或者进行更柔和的处理
            }

            $input_data_arrays[$column_name] = $post_data;

            // 确定行数,并校验所有列的数组长度是否一致
            if ($is_first_column) {
                $row_count = count($post_data);
                $is_first_column = false;
                if ($row_count === 0) {
                   log_message('info', '没有提交任何数据行。');
                   // 可以选择直接返回或显示提示信息
                   // redirect('some_page', 'refresh');
                   return;
                }
            } elseif (count($post_data) !== $row_count) {
                // 行数不一致,这是个严重问题,数据可能错位
                log_message('error', '提交的各列数据行数不一致。');
                show_error('提交的数据行数不匹配,请检查表单。');
                return;
            }
        }

        // 3. 循环行数,构建每行数据并插入
        $inserted_count = 0;
        for ($i = 0; $i < $row_count; $i++) {
            // 为每一行生成唯一的 ID (如果需要的话)
            $generated_id = $this->m_rate_template->generate_id_in_template($template_name);

            // 准备当前行的数据
            $row_data = [
                'id' => $generated_id, // 使用新生成的ID
                'id_rate' => $id_rate, // 使用固定的关联ID
            ];

            // 动态添加列数据
            foreach ($dynamic_columns as $column_name) {
                // 从对应的输入数组中取出当前行 ($i) 的值
                // 做一些基础的清理或验证
                $value = $input_data_arrays[$column_name][$i];
                // 例如,简单清理:
                $row_data[$column_name] = trim($value);
                // 你可以在这里添加更复杂的验证逻辑
            }

            // 4. 调用模型插入单行数据
            $insert_success = $this->m_rate_template->insert_rate($row_data, $template_name);

            if ($insert_success) {
                $inserted_count++;
            } else {
                log_message('error', '插入数据失败,行索引: ' . $i . ', 数据: ' . print_r($row_data, true));
                // 可以选择中断,或者记录失败信息继续处理下一行
                 show_error('处理数据时发生错误,部分数据可能未成功保存。');
                return; // 或者 break;
            }
        }

        // (可选)操作完成后可以设置闪存消息、跳转等
        $this->session->set_flashdata('success_message', "成功插入 {$inserted_count} 条记录。");
        redirect('success/page', 'refresh');

    }
}

// 假设 M_rate_template 模型中有类似这样的方法
class M_rate_template extends CI_Model {

    public function get_column($id_template) {
        // 示例:根据 id_template 查询数据库或配置文件返回列名数组
        // 请替换为实际的逻辑
        if ($id_template == 'template_A') {
            return ['min', 'max'];
        } elseif ($id_template == 'template_B') {
            return ['price', 'quantity'];
        }
        return []; // 返回空数组表示无效模板或无配置
    }

    public function get_template_table_name($id_template) {
        // 示例:根据 id_template 返回对应的数据库表名
        // 请替换为实际的逻辑
         if ($id_template == 'template_A') {
            return 'rate_details_A';
        } elseif ($id_template == 'template_B') {
            return 'rate_details_B';
        }
        return null; // 返回 null 或 false 表示无效
    }

    public function generate_id_in_template($template_name) {
        // 示例:生成唯一ID,可以使用 UUID 或其他数据库自增策略配合
        // 简单的例子 (不推荐用于生产环境,并发下可能重复)
        // return uniqid($template_name . '_');
        // 推荐使用数据库自增ID,或者可靠的UUID库
        // 如果是数据库自增,这个方法可能不需要,ID在插入后由数据库生成
        // 假设这里需要预先生成一个唯一标识符
        return $this->generate_uuid(); // 需要一个生成 UUID 的辅助函数或库
    }

    public function insert_rate($data, $table_name) {
        // 使用 CodeIgniter 的 Query Builder 插入数据
        // 它会自动处理转义,防止 SQL 注入
        return $this->db->insert($table_name, $data);
    }

    // 辅助方法示例:生成 UUID v4
    private function generate_uuid() {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000, // version 4
            mt_rand(0, 0x3fff) | 0x8000, // variant
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }
}

安全建议:

  1. 输入验证! 永远不要相信用户的输入。在将 $value 存入 $row_data 之前,进行严格的验证。比如,如果某列应该是数字,检查 is_numeric();如果是日期,尝试解析;限制字符串长度等。CodeIgniter 的 表单验证类 (Form Validation Library) 是个好帮手。
  2. SQL 注入防护: 使用 CodeIgniter 的 Query Builder ($this->db->insert()) 或预处理语句。上面的例子中 $this->db->insert() 已经能处理基本的 SQL 注入防护了,因为它会自动转义数据。确保模型方法始终使用这些安全机制。
  3. 权限检查: 确保执行此操作的用户有相应的权限。
  4. 错误处理: 代码中加入了简单的错误日志和用户反馈。对于生产环境,需要更完善的错误处理和日志记录。
  5. 数组长度检查: 代码中加入了检查所有输入数组长度是否一致的逻辑。这非常重要,否则数据会错乱。

进阶使用技巧:

  • 事务处理: 如果这批插入操作需要保证原子性(要么全部成功,要么全部失败),应该使用数据库事务。在循环插入前开启事务 ($this->db->trans_start()),结束后根据成功与否提交 ($this->db->trans_complete()$this->db->trans_commit() / $this->db->trans_rollback())。trans_complete() 会自动检测查询是否成功并提交或回滚。
  • 模型验证: 可以将验证逻辑封装到模型层,保持 Controller 的简洁。

方案二:使用 CodeIgniter 的 insert_batch()

如果数据量比较大,逐行插入可能会慢。CodeIgniter 提供了一个 insert_batch() 方法,可以一次性插入多条记录,通常效率更高。

原理和作用:

  1. 同样先获取列名、输入数组和行数。
  2. 循环行数,在每次循环里,构建好 单行 的关联数组,和方案一类似。
  3. 将每一行构建好的关联数组添加到一个主数组 $batch_data 中。
  4. 循环结束后,将这个包含所有行数据的主数组 $batch_data 传递给 $this->db->insert_batch() 方法。CodeIgniter 会生成一条包含多个值列表的 INSERT 语句(或多条,取决于驱动和数据量)。

代码示例(Controller 部分修改):

// ... (前面获取 $id_template, $id_rate, $template_name, $dynamic_columns,
//         $input_data_arrays, $row_count 的代码与方案一类似)...

// 检查是否有数据行
if ($row_count === 0) {
    log_message('info', '没有提交任何数据行用于批量插入。');
    // redirect('some_page', 'refresh'); // 或者显示消息
    return;
}

// 3. 准备批量插入的数据
$batch_data = [];
for ($i = 0; $i < $row_count; $i++) {
    // 假设不需要预先生成ID,使用数据库自增;如果需要,同方案一
    // $generated_id = $this->m_rate_template->generate_id_in_template($template_name);

    $row_data = [
        // 'id' => $generated_id, // 如果数据库自增,不需要这一行
        'id_rate' => $id_rate, // 固定关联 ID
    ];

    // 动态添加列数据
    foreach ($dynamic_columns as $column_name) {
        $value = $input_data_arrays[$column_name][$i];
        // 清理和验证数据 (同方案一)
        $row_data[$column_name] = trim($value);
    }

    // 将构建好的单行数据添加到批处理数组中
    $batch_data[] = $row_data;
}

// 4. 调用模型的批量插入方法 (或直接在Controller用 $this->db)
if (!empty($batch_data)) {
    // 建议在模型中封装批量插入逻辑
    $inserted_rows = $this->m_rate_template->insert_batch_rate($batch_data, $template_name);

    if ($inserted_rows > 0) {
        $this->session->set_flashdata('success_message', "成功批量插入 {$inserted_rows} 条记录。");
        redirect('success/page', 'refresh');
    } else {
        log_message('error', '批量插入数据失败。检查数据库日志。');
         show_error('处理数据时发生错误,未能保存数据。');
        // 批量插入失败,可能需要检查数据格式或数据库约束
    }
} else {
    // 虽然前面检查了 $row_count,但以防万一 $batch_data 是空的
    log_message('info', '没有数据被准备用于批量插入。');
    redirect('some/page', 'refresh'); // 或者显示消息
}


// --- 在 M_rate_template 模型中添加批量插入方法 ---
class M_rate_template extends CI_Model {
    // ... 其他方法 ...

    public function insert_batch_rate($data, $table_name) {
        if (empty($data) || empty($table_name)) {
            return 0; // 或 false
        }
        // 使用 CodeIgniter 的 insert_batch
        // 返回受影响的行数 (通常是插入的行数),失败可能返回 0 或 false
        return $this->db->insert_batch($table_name, $data);
    }

     // ... generate_uuid 等辅助方法 ...
}

安全建议:

同方案一。insert_batch() 同样会受益于 CodeIgniter 的查询构造器的自动转义。输入验证依然是重中之重。

进阶使用技巧:

  • 事务处理: 对于 insert_batch(),事务同样适用且推荐,确保整个批次要么成功要么失败。
  • 批次大小: 虽然 insert_batch() 比单行插入高效,但如果一次性插入成千上万条记录,可能会消耗大量内存,并且可能超过数据库允许的最大查询长度或包大小。对于非常大的数据集,可以考虑分批(chunking)插入,比如每 500 或 1000 行执行一次 insert_batch()
  • 错误定位: insert_batch() 的一个缺点是,如果批次中有任何一行数据违反了数据库约束(比如唯一键冲突),整个批次可能会失败(取决于数据库类型和设置),并且可能不容易直接定位到是哪一行或哪些行出了问题。逐行插入(方案一)更容易定位到具体的错误行。根据业务场景选择。

总的来说,面对动态列名和多行输入的插入需求,关键在于先按行重新组织数据 ,然后选择合适的插入方式(单行循环插入或批量插入)。同时,安全验证和错误处理 是任何数据库操作中都不能忽视的环节。