CodeIgniter批量插入动态列数据:从表单到数据库
2025-05-02 18:32:54
CodeIgniter 中处理动态多组输入并批量插入数据库
咱们在用 CodeIgniter 开发的时候,经常会遇到处理表单提交过来的多组数据,比如动态添加的行项目。更复杂点的情况是,这些数据的字段名(对应数据库列名)还不是固定的,需要根据某个模板 ID 来确定。这就要了命了,怎么把这些动态列名、多行数据塞进数据库呢?
别急,这事儿不难。看看下面这个场景是不是你遇到的:
你有个表单,用户可以根据选的模板 (id_template
) 动态添加好多行数据。假设模板 A 需要填 min
和 max
两列,模板 B 需要填 price
和 quantity
两列。提交后,你的 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); // 在内层循环里就插入了
}
}
这段代码的问题出在哪?
- 循环逻辑不对: 代码是按 列 来循环的。它先把所有
min
的值,每次一个,构造成一条记录(只有min
列有值)插入;然后再把所有max
的值,每次一个,构造成另一条记录(只有max
列有值)插入。这显然不是我们想要的按 行 插入(希望的是{min: 10, max: 100}
一行,{min: 20, max: 200}
一行...)。 - 效率低下: 内层循环每遍历一个值就执行一次数据库插入操作。如果提交了 3 行数据,每行 2 列,这就导致了
3 * 2 = 6
次数据库插入,而不是理想中的 3 次。数据量大的时候,性能会很差。 - ID 生成问题:
generate_id_in_template
在每次内层循环都调用,可能会为本应属于同一逻辑行(比如min: 10
和max: 100
)的不同片段生成不同的 ID,或者造成不必要的 ID 生成开销。id_rate
同样面临被重复用于不同插入片段的问题。
明白了问题所在,咱们就能对症下药了。
解决方案
核心思路是调整数据结构和循环方式,先按 行 组织好数据,再一次性或者逐行插入。
方案一:重构数据,逐行插入
这个方法比较直观,先根据提交的数据重新组织成一行一行的数据结构,然后循环这个行数组,每次插入一条完整的记录。
原理和作用:
- 先获取所有动态列名。
- 获取每个动态列对应的输入数组。
- 确定有多少行数据需要插入(通常可以取第一个输入数组的长度)。
- 循环这个行数,在每次循环里,根据当前的行索引,从所有输入数组中取出对应位置的值,组合成一个包含所有动态列和固定列(如
id
,id_rate
)的关联数组。 - 调用模型的方法,插入这一行数据。
代码示例(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)
);
}
}
安全建议:
- 输入验证! 永远不要相信用户的输入。在将
$value
存入$row_data
之前,进行严格的验证。比如,如果某列应该是数字,检查is_numeric()
;如果是日期,尝试解析;限制字符串长度等。CodeIgniter 的 表单验证类 (Form Validation Library) 是个好帮手。 - SQL 注入防护: 使用 CodeIgniter 的 Query Builder (
$this->db->insert()
) 或预处理语句。上面的例子中$this->db->insert()
已经能处理基本的 SQL 注入防护了,因为它会自动转义数据。确保模型方法始终使用这些安全机制。 - 权限检查: 确保执行此操作的用户有相应的权限。
- 错误处理: 代码中加入了简单的错误日志和用户反馈。对于生产环境,需要更完善的错误处理和日志记录。
- 数组长度检查: 代码中加入了检查所有输入数组长度是否一致的逻辑。这非常重要,否则数据会错乱。
进阶使用技巧:
- 事务处理: 如果这批插入操作需要保证原子性(要么全部成功,要么全部失败),应该使用数据库事务。在循环插入前开启事务 (
$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()
方法,可以一次性插入多条记录,通常效率更高。
原理和作用:
- 同样先获取列名、输入数组和行数。
- 循环行数,在每次循环里,构建好 单行 的关联数组,和方案一类似。
- 将每一行构建好的关联数组添加到一个主数组
$batch_data
中。 - 循环结束后,将这个包含所有行数据的主数组
$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()
的一个缺点是,如果批次中有任何一行数据违反了数据库约束(比如唯一键冲突),整个批次可能会失败(取决于数据库类型和设置),并且可能不容易直接定位到是哪一行或哪些行出了问题。逐行插入(方案一)更容易定位到具体的错误行。根据业务场景选择。
总的来说,面对动态列名和多行输入的插入需求,关键在于先按行重新组织数据 ,然后选择合适的插入方式(单行循环插入或批量插入)。同时,安全验证和错误处理 是任何数据库操作中都不能忽视的环节。