使用单个查询插入多行时获取所有插入的 ID
Posted
技术标签:
【中文标题】使用单个查询插入多行时获取所有插入的 ID【英文标题】:Get all inserted IDs when inserting multiple rows using a single query 【发布时间】:2013-05-08 14:31:33 【问题描述】:我已经看过其他答案,但我仍然觉得我的问题是相关的,值得单独输入。
我有一个名为 settings 的表(它存储用户设置),我必须为每个用户插入多个设置。最初,我为每个设置执行了单独的插入语句,但觉得这不是一个特别好的方法,我想通过同一个插入语句插入多行。我唯一的问题是我想要每个新插入的行的 auto_incremented ID。
我已经阅读了说这是不可能/可扩展等的答案,但我觉得我已经找到了解决方案。我想要反馈我的方式是否正确,因此这个问题。
我所做的很简单。插入多行后,我调用 last_insert_id() 来获取同时插入的行的第一行的 ID。我已经知道插入的行数,所以我只需创建一个新数组并使用从 last_insert_id() 开始并以 last_insert_id()+n-1 结束的 ID 填充它(其中 n 是插入的行数)。
我觉得这会起作用,原因如下:
1.) mysql 文档指出 last_insert_id() 依赖于连接,如果另一个客户端/连接插入新记录,则不会影响其他客户端的 last_insert_id()。
2.) 我觉得由于插入是由单个 SQL 语句完成的,所以整个插入应该被视为单个事务。如果这是真的,那么应该应用 ACID 规则并且 auto_incremented 值应该是连续的。我不确定这个。
这就是我认为逻辑应该有效的原因。所以我的问题是,上述逻辑是否适用于所有条件?我可以依靠它在所有情况下正常工作吗?我知道它目前对我有用。
【问题讨论】:
我也不确定第 2 点。如果是我,我会考虑创建一个临时表,将每个 last_insert_id 添加到其中,然后返回它。或者更好的是,有另一种选择刚刚发生的事情的方式,(用户并说一个 datelastchanged 时间戳) 简单的答案没有。它并不健壮,虽然任何单独的 last_insert_id() 可能依赖于连接,但它不提供保证所有插入都将在并发块中并且不会与来自另一个并发进程的插入交错。在编程中,依赖健壮的工程比感觉应该没问题要好得多。 我同意上述问题。但是,您要插入的表上可能有一个额外的字段,并将其放入该唯一标识符中。然后,您可以读取具有该唯一标识符的所有记录的该表,以获取插入的 id 列表。我仍然会担心哪个 id 引用了您插入的哪组数据是未定义的。 【参考方案1】:如果你喜欢赌博 - 那就这样做吧:)
要 99% 确定您必须锁定表才能写入。您不确定(将来)这两个交易是否能够交织在一起。
100% 确保您阅读了这些值。 (或者分析源码MySQL) 最好的解决方案是将日期添加到 tablei 编辑设置并阅读最新的。如果不想改变结构,可以使用触发器http://dev.mysql.com/doc/refman/5.0/en/create-trigger.html。
好的解决方案是刷新所有设置或仅刷新对:键 - 设置名称
【讨论】:
【参考方案2】:不应依赖此行为;除了明显的锁定问题外,假设您要设置 mastermaster 复制;突然之间,id 每次递增 2。
除此之外,与其实际编写多个插入语句,不如使用准备好的语句:
$db = new PDO(...);
$db->beginTransaction();
$stmt = $db->prepare('INSERT INTO `mytable` (a, b) VALUES (?, ?)');
foreach ($entries as $entry)
$stmt->execute(array($entry['a'], $entry['b']));
$id = $db->lastInsertId();
$db->commit();
【讨论】:
这会导致实际的数据库访问吗?与没有事务的插入相比,性能是否更差? @Sangoku 性能不应该是您使用事务时的首要考虑因素;数据一致性是。 在一种情况下,我正在研究它。如果有人想知道性能。它快了 4 倍。 :)【参考方案3】:我同意@anigel。您不能确定某些交易不会混淆。您可以将插入拆分为单独的查询,为每个单独的查询调用 last_insert_id() 并使用结果填充数组。
当然,这可能会增加处理时间,但至少可以确保避免冲突。另外,由于它是一个设置表,您不太可能必须为每个客户端请求运行大量事务
【讨论】:
【参考方案4】:怎么样:
$id = array();
$sql = "INSERT INTO `table` VALUES ('', 'foo', 'bar');";
$sql .= "INSERT INTO `table` VALUES ('', 'foo', 'bar');";
$sql .= "INSERT INTO `table` VALUES ('', 'foo', 'bar');";
$sql .= "INSERT INTO `table` VALUES ('', 'foo', 'bar');";
$sql .= "INSERT INTO `table` VALUES ('', 'foo', 'bar');";
$sql .= "INSERT INTO `table` VALUES ('', 'foo', 'bar');";
if ($db=new mysqli('host', 'user', 'pass', 'dbase'))
$db->multi_query($sql);
if ( isset($db->insert_id) ) $id[] = $db->insert_id;
while ($db->more_results())
if ($db->next_result()) $id[] = $db->insert_id;
else trigger_error($db->error);
$db->close();
else trigger_error($db->error);
if (count($id) == 0) trigger_error('Uh oh! No inserts succeeded!');
else print_r($id);
也许这会像您想要的那样返回您的插入 ID。我没有测试过它,但也许你可以根据你的目的调整它,谁知道它可能真的有效。
【讨论】:
最后插入的 id 是基于会话的变量,所以下一个会覆盖前一个。 @Jack 你的意思是在每个查询运行之后和在 while 语句中循环之前所有先前的 insert_id 已经是历史记录并且没有与结果一起维护吗? 我唯一可以建议的另一件事是 INSERTS 之间的SELECT LAST_INSERT_ID()
语句,但这也不是一个完美的解决方案。在我看来,如果您必须拥有插入行的 ID,则唯一的方法是使用单独的语句。【参考方案5】:
我已经这样做了,这是我的应用程序。我在循环中插入记录,当插入每条记录时,我将通过调用 last_insert_id() 将自动增量 id 存储在数组中。所以我可以在任何需要的地方使用插入的 id 数组。
【讨论】:
这是在回答什么问题?【参考方案6】:我做了一点,但最终没有发现有用的数据所以停止了。
我跟踪表中每个条目的日期时间和 user_who_altered,因此检索列表变得简单。
但是,重要的是设置一个带有时间的变量,而不是依赖 NOW():
INSERT INTO table (username,privelege,whenadded,whoadded) VALUES ('1','2','$thetime','$theinserter'),('1','5','$thetime','$theinserter'),etc...;
会很好地插入多行。
检索您寻找的数组:
SELECT idrow FROM table WHERE username='1' AND whenadded='$thetime' AND whoadded='$theinserter';
这是一种很好的做法,因为您可以跟踪哪个用户修改了记录,并且它不依赖于锁定或机会。
至于您自己的解决方案,它将起作用并保存查询。我会担心它如何响应繁忙表上的准备好的语句,也许使用的方法应该考虑到这一点。我使用的方法对此类问题免疫。
【讨论】:
【参考方案7】:这个问题一直困扰着我……你为什么需要每行的 insert_id?在我看来,如果您要插入设置表然后将设置与用户相关联,最好在表中添加某种用户密钥。我可能只是没有看到全貌,但这是我得到的想法:
+---------------------------------------+
| `settings` |
+------+--------+-----------+-----------+
| id | user | setting | value |
+------+--------+-----------+-----------+
| `id` INT(11) PRIMARY AUTO_INCREMENT |
+---------------------------------------+
| `user` INT(11) |
+---------------------------------------+
| `setting` VARCHAR(15) |
+---------------------------------------+
| `value` VARCHAR(25) |
+---------------------------------------+
+---------------------------------------+
| `users` |
+------+--------+--------+--------------+
| id | name | email | etc |
+------+--------+--------+--------------+
| `id` INT(11) PRIMARY AUTO_INCREMENT |
+---------------------------------------+
| `name` VARCHAR(32) |
+---------------------------------------+
| `email` VARCHAR(64) |
+---------------------------------------+
| `etc` VARCHAR(64) |
+---------------------------------------+
我在这里的基本想法是,您对 insert_id(s) 所做的事情就是如何尝试在用户和设置之间创建引用,以便您知道谁设置了什么,然后您将他们的设置返回给他们。
如果我完全失去了重点,请引导我回到主题上,我会尝试提出另一个解决方案,但是如果我在接近你想要去的地方找到任何地方:
如果您确实尝试使用 insert_id(s) 关联这两个表,那么类似下面的代码不会更有意义(假设您有一个类似于上面的表结构):
我假设 $user 是对特定用户 (users
.id
) 的引用。
我还假设您有一个名为 $settings 的关联数组,您正尝试将其放入数据库中。
$mysqli = new mysqli('host', 'username', 'password', 'database') or trigger_error('[MYSQLI]: Could not connect.');
if ($mysqli)
foreach ($settings AS $setting_name=>$setting_value)
// I'll do individual queries here so error checking between is easy, but you could join them into a single query string and use a $mysqli->multi_query();
// I'll give two examples of how to make the multi_query string below.
$sql = "INSERT INTO `settings` (`id`, `user`, `setting`, `value`) VALUES ('', $user, '$setting_name', '$setting_value')";
$mysqli->query($sql) or trigger_error('[MYSQLI]: ' . $mysqli->error . '['.$sql.']';
$mysqli->close() // Since we know in this scope that $mysqli was created we need to close it when we are finished with it.
现在,当您需要用户设置时,您可以使用 JOIN 执行 SELECT 以将所有设置和用户信息放在一起,如果我对此感到不满,请原谅我,因为我目前还不是 mysql(i) 专家,所以我不确定你是否需要做任何特别的事情,因为设置表中有多个条目,而用户表中有一个条目,但我敢肯定,如果我搞砸了,有人可以让我们俩都直截了当:
$mysqli = new mysqli('host', 'username', 'password', 'database') or trigger_error('[MYSQLI]: Unable to connect.');
if ($mysqli)
$sql = "SELECT `users`.`name`, `users`.`email`, `users`.`id`, `users`.`etc`, `settings`.`setting`, `settings`.`value` FROM `settings` JOIN (`users`) ON (`users`.`id`=`settings`.`user`) WHERE `settings`.`user`=$user GROUP BY `settings`.`user` ORDER BY `settings`.`user` ASC";
if ($result=$mysqli->query($sql))
if ($result->num_rows == 0)
echo "Uh oh! $user has no settings or doesn't exist!";
else
// Not sure if my sql would get multiple results or if it would get it all in one row like I want so I'll assume multiple which will work either way.
while ($row=$result->fetch_array())
print_r($row); // Just print the array of settings to see that it worked.
$result->free(); // We are done with our results so we release it back into the wild.
else trigger_error('[MYSQLI]: '.$mysqli->error . '['.$sql.']');
$mysqli->close(); // Our if ($mysqli) tells us that in this scope $mysqli exists, so we need to close it since we are finished.
正如我之前所说,我目前还不是 mysql(i) 专家,可能有一些方法可以简化事情,我可能在我的 JOIN 语句的语法上犯了一些错误,或者只是使用了多余的子句,比如分组依据。我将 SELECT 从settings
中提取出来,并将users
加入其中,因为我不确定将设置加入用户是否会生成一个包含用户以及与我们的 WHERE 子句匹配的设置中的所有可能值的单一结果。我只加入过A
和B
,每个都有 1 个结果,但我确信 JOIN 是在正确的一般区域。
除了多个查询之外,我说过我会给出为 multi_query 构建单个查询字符串的示例,所以:
$arr = array();
foreach($settings AS $setting_name=>$setting_value)
$arr[] = "INSERT INTO `settings` (`id`, `user`, `setting`, `value`) VALUES ('', $user, '$setting_name', '$setting_value')";
$sql = join('; ', $arr);
或
foreach($settings AS $setting_name=>$setting_value)
$sql = ( isset($sql) ) ? $sql.'; '."INSERT INTO `settings` (`id`, `user`, `setting`, `value`) VALUES ('', $user, '$setting_name', '$setting_value')" : "INSERT INTO `settings` (`id`, `user`, `setting`, `value`) VALUES ('', $user, '$setting_name', '$setting_value')";
我要注意的最后一件事:除非您特别需要用户具有相同设置的多个条目,否则您可以在 INSERT 查询之前执行 UPDATE 并且仅在结果 $mysqli->insert_id = 时才执行 INSERT = 0。如果您尝试为用户保留设置更改的历史记录,这就是您需要多个条目的原因,那么我建议创建一个单独的表,其中包含如下表结构的日志:
+--------------------------------------------------+
| `logs` |
+------+--------+--------+-----------+-------------+
| id | time | user | setting | new_value |
+------+--------+--------+-----------+-------------+
| `id` INT(11) PRIMARY AUTO_INCREMENT |
+--------------------------------------------------+
| `time` TIMESTAMP |
+--------------------------------------------------+
| `user` INT(11) |
+--------------------------------------------------+
| `setting` INT(11) |
+--------------------------------------------------+
| `new_value` VARCHAR(25) |
+--------------------------------------------------+
如果您将 CURRENT_TIMESTAMP 设为默认值 time
或仅将 date('Ymd G:i:s') 插入该字段,则您可以通过在创建新设置时插入日志来跟踪设置更改或更改。
如果 UPDATE 没有更改任何内容,则要执行 INSERTS,您可以使用两个单独的语句。也可以使用“INSERT VALUES() ON DUPLICATE KEY UPDATE ...”来执行此操作,但显然不建议在具有多个 UNIQUE 字段的表上使用此方法。使用后一种方法意味着单个查询,但您必须组合 user
和 setting
UNIQUE(或者可能是 DISTINCT?)。如果您创建了setting
UNIQUE,那么除非我弄错了,否则您将只能在表中拥有一个setting
='foo'。要使用两个语句来做到这一点,您可以执行以下操作:
$sql = "UPDATE `settings` SET `value`='bar' WHERE `user`=$user AND `setting`='foo' LIMIT 1";
$mysqli->query() or trigger_error('[MYSQLI]: ' . $mysqli->error . '['.$sql.']');
if ( $mysqli->affected_rows == 0 )
$sql = "INSERT INTO `settings` (`id`, `user`, `setting`, `value`) VALUES ('', $user, 'foo', 'bar')";
$mysqli->query($sql) or trigger_error('[MYSQLI]: ' . $mysqli->error . '['.$sql.']');
在上面的代码中,如果你愿意,你可以用 $mysqli->insert_id 代替 $mysqli->affected_rows,但最终结果是一样的。仅当 UPDATE 未更改表中的任何内容(这表明该设置和该用户没有记录)时才调用 INSERT 语句。
很抱歉,这是一个非常冗长的回复,并且偏离了您最初的问题,但我希望它与您的真正目的/目标相关。我期待您的回复和 cmet 关于如何从真正的 SQL 大师那里改进我的 SQL 语句的方法。
【讨论】:
以上是关于使用单个查询插入多行时获取所有插入的 ID的主要内容,如果未能解决你的问题,请参考以下文章