MySQLではinsertで生成されるIDが帰ってこない問題へのアプローチ

注）mysql 5.6のことだけ調べました。他のバージョンのことはよくわからないです。

# この記事でできるようになること
mysqlでinsertしたレコードの自動生成されたIDの取得

# 解決したかった問題
Railsでbulk insertする場合

- 生SQLでinsert文流す
- activerecord-importを使う

を使うのかなと思うのですが、mysqlは生成したレコードのIDがわからない、、、

生成したIDで紐付け処理を行う場合は、再度fetchしてIDを取得する必要がある。
が、取得する場合にkey値がなかったらどうすればいいのか

## こんなデータ

```sql
mysql> desc parents;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)        | NO   |     | NULL    |                |
| created_at | datetime            | NO   |     | NULL    |                |
| updated_at | datetime            | NO   |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+

mysql> desc children;
+------------+---------------------+------+-----+---------+----------------+
| Field      | Type                | Null | Key | Default | Extra          |
+------------+---------------------+------+-----+---------+----------------+
| id         | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255)        | NO   |     | NULL    |                |
| parent_id  | bigint(20) unsigned | NO   | MUL | NULL    |                |←これがfkになっている。
| created_at | datetime            | NO   |     | NULL    |                |
| updated_at | datetime            | NO   |     | NULL    |                |
+------------+---------------------+------+-----+---------+----------------+
(CONSTRAINT `fk_child_parent` FOREIGN KEY (`parent_id`) REFERENCES `parents` (`id`) がついているイメージ)
```

railsだと

```ruby
class Parent < ApplicationRecord
  has_many :children
end

class Child < ApplicationRecord
  belongs_to :parent
end
```

## こんな処理

```ruby
def insert_many_times
    children = (1..10).flat_map do |i|
        (1..2).map do |j|
        Child.new(name: "name_#{i}_#{j}").tap { |c| c.parent = Parent.new(name: "name_#{i}") }
        end
    end
    children.each(&:save)
end

irb(main):01:0> insert_many_times

TRANSACTION (0.2ms)  BEGIN
Parent Create (6.3ms)  INSERT INTO `parents` (`name`, `created_at`, `updated_at`) VALUES ('name_1', '2021-08-27 02:45:05', '2021-08-27 02:45:05')
Child Create (4.3ms)  INSERT INTO `children` (`name`, `parent_id`, `created_at`, `updated_at`) VALUES ('name_1_1', 72, '2021-08-27 02:45:05', '2021-08-27 02:45:05')
TRANSACTION (6.9ms)  COMMIT

以下同じinsert分が10回流れる。。。。
```

mysqlはinsertのSQLでidって返ってきません。
postgresqlだったらとれるんですが、、、

```sql
mysql> insert into parents(name, created_at, updated_at) values('aaa',now(),now());
Query OK, 1 row affected (0.01 sec)
```

# 作成されたレコードのIDの取得

mysqlでinsertされたidを取得する方法を調べてみる。
https://dev.mysql.com/doc/refman/5.6/ja/mysql-insert-id.html
> 前の INSERT または UPDATE ステートメントによって、AUTO_INCREMENT カラムに生成された値を返します。この関数は、AUTO_INCREMENT フィールドを含むテーブルへのINSERT ステートメントを実行したか、INSERT または UPDATE を使用して、LAST_INSERT_ID(expr) によってカラム値を設定した後に使用します。
> mysql_insert_id() の値は、現在のクライアント接続内で発行されたステートメントによってのみ影響を受けます。それは、ほかのクライアントによって発行されたステートメントに影響を受けません。

なるほど。こいつは使えそうだ。

> mysql_insert_id() は、AUTO_INCREMENT 値を生成するストアドプロシージャーの CALL ステートメントのあとに、0 を返します。この場合、mysql_insert_id() が CALL に適用され、プロシージャー内のステートメントには適用されないためです。プロシージャー内で、LAST_INSERT_ID() を SQL レベルで使用して、AUTO_INCREMENT 値を取得できます。
> LAST_INSERT_ID() と mysql_insert_id() の違いの理由は、LAST_INSERT_ID() がスクリプトで使いやすく、mysql_insert_id() は AUTO_INCREMENT カラムに起こったことに関してより正確な情報を提供しようとすることです。

## 試す

LAST_INSERT_IDを試す。

```sql
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                0 |
+------------------+
                                                        mysql> SELECT LAST_INSERT_ID();
                                                        +------------------+
                                                        | LAST_INSERT_ID() |
                                                        +------------------+
                                                        |                0 |
                                                        +------------------+
                                                        1 row in set (0.00 sec)
mysql> insert into parents(name, created_at, updated_at) values('aaa',now(),now());
Query OK, 1 row affected (0.01 sec)

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|               94 |
+------------------+
                                                        mysql> SELECT LAST_INSERT_ID();
                                                        +------------------+
                                                        | LAST_INSERT_ID() |
                                                        +------------------+
                                                        |                0 |
                                                        +------------------+
                                                        1 row in set (0.00 sec)
mysql> SELECT id FROM parents ORDER BY id DESC LIMIT 1;
+----+
| id |
+----+
| 94 |
+----+
                                                        mysql> insert into parents(name, created_at, updated_at) values('aaa',now(),now());
                                                        Query OK, 1 row affected (0.00 sec)

                                                        mysql> SELECT LAST_INSERT_ID();
                                                        +------------------+
                                                        | LAST_INSERT_ID() |
                                                        +------------------+
                                                        |               95 |
                                                        +------------------+
                                                        1 row in set (0.00 sec)

mysql> SELECT id FROM parents order by id desc limit 1;
+----+
| id |
+----+
| 95 |
+----+

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|               94 |
+------------------+
```
欲しかったものはこれだ。

## Railsでやってるのは何？

（コードリーディング間違えてるかもなので、興味がある方はソース見てみてください）

```ruby
# lib/activerecord/connection_adapters/abstract/database_statements.rb:156
#
# Executes an INSERT query and returns the new record's ID
#
# +id_value+ will be returned unless the value is +nil+, in
# which case the database will attempt to calculate the last inserted
# id and return that value.
#
# If the next id was calculated in advance (as in Oracle), it should be
# passed in as +id_value+.
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
    sql, binds = to_sql_and_binds(arel, binds)
    value = exec_insert(sql, name, binds, pk, sequence_name)
    id_value || last_inserted_id(value)
end

last_inserted_idが怪しい

# lib/activerecord/connection_adapters/mysql/database_statements.rb:156
def last_inserted_id(result)
    @connection.last_id
end

last_idが怪しい


def connect
    @connection = self.class.new_client(@config)
    configure_connection
end

def new_client(config)
    Mysql2::Client.new(config)
        ~~~
end
```
mysql2のgemに飛ぶ。
https://github.com/brianmario/mysql2/blob/master/ext/mysql2/client.c:1026

```c
/* call-seq:
 *    client.last_id
 *
 * Returns the value generated for an AUTO_INCREMENT column by the previous INSERT or UPDATE
 * statement.
 */
static VALUE rb_mysql_client_last_id(VALUE self) {
  GET_CLIENT(self);
  REQUIRE_CONNECTED(wrapper);
  return ULL2NUM(mysql_insert_id(wrapper->client));
}

あった。
おんなじことしてそう。
```

## 実験（並行で処理）

並行でリクエストきてる様子をみてみる。

```ruby
  # 先に実行して、トランザクションで処理を遅らせる。
  def test1
    families = (1..10).map do |i|
      [Parent.new(name: "name_#{i}"), (1..2).map { |j| Child.new(name: "name_#{i}_#{j}") }]
    end

    parents = families.map { |p, _| p }
    children = families.flat_map { |_, c| c }
    p Time.current
    ActiveRecord::Base.transaction do
      Parent.import(parents)
      (1..5).each { |i| p i; sleep 1 }
      ids = fetch_last_insert_ids
      related_children = make_relation(ids, children)
      Child.import(related_children)
    end
    p Time.current
  end

  # 後に実行する
  def test2
    families = (21..25).map do |i|
      [Parent.new(name: "name_#{i}"), (1..2).map { |j| Child.new(name: "name_#{i}_#{j}") }]
    end
    parents = families.map { |p, _| p }
    children = families.flat_map { |_, c| c }
    p Time.current
    ActiveRecord::Base.transaction do
      Parent.import(parents)
      ids = fetch_last_insert_ids
      related_children = make_relation(ids, children)
      Child.import(related_children)
    end
    p Time.current
  end

  # 親子の関係を作る
  def make_relation(ids, children)
    relation = 0
    ids.each do |id|
      2.times.each do |_|
        children[relation].parent_id = id
        relation += 1
      end
    end
    children
  end

  # 重要な確認箇所
  def fetch_last_insert_ids
    query = 'SELECT LAST_INSERT_ID() AS id, ROW_COUNT() as row'
    id, row = ActiveRecord::Base.connection.select_one(query).values

    row.times.map { |num| id + num }
  end

irb(main):019:0> Test.test1
Fri, 27 Aug 2021 15:57:42.920741000 JST +09:00
  TRANSACTION (0.2ms)  BEGIN
  Parent Create Many (6.2ms)  INSERT INTO `parents` (`id`,`name`,`created_at`,`updated_at`) VALUES (NULL,'name_1','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_2','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_3','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_4','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_5','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_6','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_7','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_8','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_9','2021-08-27 15:57:42','2021-08-27 15:57:42'),(NULL,'name_10','2021-08-27 15:57:42','2021-08-27 15:57:42')

# 5秒間待ち

                                        irb(main):027:0> Test.test2
                                        Fri, 27 Aug 2021 15:57:44.068953000 JST +09:00
                                        TRANSACTION (0.3ms)  BEGIN
                                        Parent Create Many (0.4ms)  INSERT INTO `parents` (`id`,`name`,`created_at`,`updated_at`) VALUES (NULL,'name_21','2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_22','2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_23','2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_24','2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_25','2021-08-27 15:57:44','2021-08-27 15:57:44')
                                        (0.9ms)  SELECT LAST_INSERT_ID() AS id, ROW_COUNT() as row
parent_idが106からの連番になっている
                                        Child Create Many (1.9ms)  INSERT INTO `children` (`id`,`name`,`parent_id`,`created_at`,`updated_at`) VALUES (NULL,'name_21_1',106,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_21_2',106,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_22_1',107,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_22_2',107,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_23_1',108,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_23_2',108,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_24_1',109,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_24_2',109,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_25_1',110,'2021-08-27 15:57:44','2021-08-27 15:57:44'),(NULL,'name_25_2',110,'2021-08-27 15:57:44','2021-08-27 15:57:44')
                                        TRANSACTION (5.0ms)  COMMIT
                                        Fri, 27 Aug 2021 15:57:44.137688000 JST +09:00
# 5秒間待終わり
   (0.3ms)  SELECT LAST_INSERT_ID() AS id, ROW_COUNT() as row
  Child Create Many (0.5ms)  INSERT INTO `children` (`id`,`name`,`parent_id`,`created_at`,`updated_at`) VALUES (NULL,'name_1_1',96,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_1_2',96,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_2_1',97,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_2_2',97,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_3_1',98,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_3_2',98,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_4_1',99,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_4_2',99,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_5_1',100,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_5_2',100,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_6_1',101,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_6_2',101,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_7_1',102,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_7_2',102,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_8_1',103,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_8_2',103,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_9_1',104,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_9_2',104,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_10_1',105,'2021-08-27 15:57:47','2021-08-27 15:57:47'),(NULL,'name_10_2',105,'2021-08-27 15:57:47','2021-08-27 15:57:47')
  TRANSACTION (0.5ms)  COMMIT
Fri, 27 Aug 2021 15:57:48.032617000 JST +09:00
parent_idが96からの連番になっている

irb(main):020:0> Parent.find(100)
  Parent Load (3.7ms)  SELECT `parents`.* FROM `parents` WHERE `parents`.`id` = 100 LIMIT 1
=> #<Parent id: 100, name: "name_5", created_at: "2021-08-27 15:57:42.000000000 +0900", updated_at: "2021-08-27 15:57:42.000000000 +0900">
irb(main):021:0> Parent.find(100).children
  Parent Load (0.4ms)  SELECT `parents`.* FROM `parents` WHERE `parents`.`id` = 100 LIMIT 1
  Child Load (4.4ms)  SELECT `children`.* FROM `children` WHERE `children`.`parent_id` = 100 /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy [#<Child id: 179, name: "name_5_1", parent_id: 100, created_at: "2021-08-27 15:57:47.000000000 +0900", updated_at: "2021-08-27 15:57:47.000000000 +0900">, #<Child id: 180, name: "name_5_2", parent_id: 100, created_at: "2021-08-27 15:57:47.000000000 +0900", updated_at: "2021-08-27 15:57:47.000000000 +0900">]>
```
nameをみた感じだと紐付けも期待通り

# 注意点

https://dev.mysql.com/doc/refman/5.6/ja/innodb-auto-increment-configurable.html
> InnoDB では AUTO_INCREMENT カラムを含むテーブルへの挿入を行う際に、AUTO-INC ロックと呼ばれる特殊なテーブルレベルロックが使用されます。このロックは通常、指定された一連の INSERT ステートメントに予測可能かつ繰り返し可能な順序で自動インクリメント番号が割り当てられるように、(トランザクションが終了するまでではなく) ステートメントが終了するまで保持されます。

と書いてあるので、一つのinsertステートメント内であれば、連番は問題なさそう

https://dev.mysql.com/doc/refman/5.6/ja/information-functions.html#function_row-count
> SELECT 以外の DML ステートメント: 影響を受ける行数です。これは、(以前と同様に) UPDATE、INSERT、DELETE などのステートメントに適用されますが、ALTER TABLE や LOAD DATA INFILE などのステートメントにも適用されるようになりました。

とあるので、insert直後に行うことに気をつけなければいけません。


また、

https://dev.mysql.com/doc/refman/5.6/ja/information-functions.html#function_row-count
> innodb_autoinc_lock_mode = 2 (「インターリーブ」 ロックモード)
> このロックモードでは、テーブルレベル AUTO-INC ロックを使用する 「INSERT のような」ステートメントは 1 つも存在しない
> 任意のステートメントによって挿入される行に対して生成された値が連続的でない可能性があります。

```sql
SELECT @@innodb_autoinc_lock_mode;
```
で値を要チェックしてください



# 結論

mysqlでbulk流すときは

```sql
SELECT LAST_INSERT_ID() AS id, ROW_COUNT() as row
```

が使えそう！