generated column ＋ 外部キーをいろいろ試してみた

# generated column ＋ 外部キーをいろいろ試してみた

※ mysql 5.7.10 で試しています

## 準備

実験用テーブルを用意します。

```
DROP TABLE IF EXISTS table3;
DROP TABLE IF EXISTS table2;
DROP TABLE IF EXISTS table1;

CREATE TABLE table1 (
  id INT(11) NOT NULL AUTO_INCREMENT,
  fkval1 INT(11) NULL DEFAULT NULL,
  PRIMARY KEY (id),
  INDEX idx_table1 (fkval1)
);

CREATE TABLE table2 (
  id INT(11) NOT NULL AUTO_INCREMENT,
  val INT(11),
  fkval2 INT(11) AS (val * 2) STORED,
  PRIMARY KEY (id),
  INDEX idx_table2 (fkval2),
  CONSTRAINT fk1 FOREIGN KEY (fkval2) REFERENCES table1 (fkval1)
);

CREATE TABLE table3 (
  id INT(11) NOT NULL AUTO_INCREMENT,
  fkval3 INT(11) NULL DEFAULT NULL,
  PRIMARY KEY (id),
  CONSTRAINT fk2 FOREIGN KEY (fkval3) REFERENCES table2 (fkval2) ON UPDATE CASCADE ON DELETE CASCADE
);
```

`table1.fkval1` -> `table2.fkval2` -> `table3.fkval3` という親子関係です。
`table2.fkval2` は `val` を元にした generated column です(2倍の値)。
`id` はただの癖なので特に意味はありません。

## 実験(成功)

これらのテーブルにレコードを挿入してみます。

```
INSERT INTO table1 (fkval1) VALUES (10);
INSERT INTO table2 (val) VALUES (5);
INSERT INTO table3 (fkval3) VALUES (10);
```

特にエラーは出ません。

table1 に制約は存在しないため、fkval1 はなんでも入ります。ここでは 10 を入れています。

table2 は table1 への外部キー制約が存在するため、fkval2 の値は ↑で入れた 10 の値でなければなりません。
エラーはあとで試すのでとりあえず入るであろう 5 を入れています。

table3 は table2 への外部キー制約が存在するため、fkval3 の値は ↑で入れた 10 の値でなければなりません。
エラーはあとで試すのでとりあえず入るであろう 10 を入れています。

## 実験(失敗)

次に外部キー制約に引っかかるようなレコードを入れてみます。

```
INSERT INTO table2 (val) VALUES (10);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`table2`, CONSTRAINT `fk1` FOREIGN KEY (`fkval2`) REFERENCES `table1` (`fkval1`))
```

怒られました。
table2.val に 10 を突っ込むと fkval2 が20になり、外部キーに違反するからです。
これにより、 generated column を外部キーの参照側にしても正常に動作していることがわかります。

```
INSERT INTO table3 (fkval3) VALUES (20);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`table3`, CONSTRAINT `fk2` FOREIGN KEY (`fkval3`) REFERENCES `table2` (`fkval2`))
```

怒られました。
table2.fkval2 に 20 なんてレコードはないため、 table3.fkval3 に 20 を突っ込もうとして外部キー違反になるからです。
これにより、 generated column を外部キーの被参照側にしても正常に動作していることがわかります。

逆に言えば `INSERT INTO table1 (fkval1) VALUES (20)` を実行した後は上の2クエリは成功することになります。
実験(成功)と意味的には同じですが一応試しておきます。

```
INSERT INTO table1 (fkval1) VALUES (20);
Query OK, 1 row affected (0.00 sec)
INSERT INTO table2 (val) VALUES (10);
Query OK, 1 row affected (0.00 sec)
INSERT INTO table3 (fkval3) VALUES (20);
Query OK, 1 row affected (0.00 sec)
```

OK のようです。

## 参照アクション

ON UPDATE, ON DELETE を省略（RESTRICT）してるので他の参照アクションを試してみます。

```
ALTER TABLE table2 DROP FOREIGN KEY fk1;
ALTER TABLE table2 ADD CONSTRAINT fk1 FOREIGN KEY (fkval2) REFERENCES table1 (fkval1) ON UPDATE CASCADE ON DELETE CASCADE;
ERROR 3104 (HY000): Cannot define foreign key with ON UPDATE CASCADE clause on a generated column.
```

怒られました。CASCADE にはできないようです。

http://dev.mysql.com/doc/refman/5.7/en/innodb-foreign-key-constraints.html に記載がありますが、確かに試してみたところ、RESTRICT か NOT ACTION しか設定できないようです。
ただ「ON UPDATE CASCADE はダメだよ！」とも言っているので、 ON DELETE だけを試したところ普通に行けるようでした。

```
ALTER TABLE table2 ADD CONSTRAINT fk1 FOREIGN KEY (`fkval2`) REFERENCES `table1` (`fkval1`) ON UPDATE RESTRICT ON DELETE CASCADE;
Query OK, 2 rows affected (0.10 sec)
Records: 2  Duplicates: 0  Warnings: 0
```

generated column を外部キーの参照側にすることはめったに無いと思うし、個人的には ON UPDATE は割りとどうでもよくて ON DELETE の方が大事だと思うのでこれでも十分嬉しかったりします。
一応効いているか試してみます。

```
SELECT
  (SELECT COUNT(*) FROM table1) as table1count,
  (SELECT COUNT(*) FROM table2) as table2count,
  (SELECT COUNT(*) FROM table3) as table3count;
+-------------+-------------+-------------+
| table1count | table2count | table3count |
+-------------+-------------+-------------+
|           2 |           2 |           2 |
+-------------+-------------+-------------+

DELETE FROM table1;

SELECT
  (SELECT COUNT(*) FROM table1) as table1count,
  (SELECT COUNT(*) FROM table2) as table2count,
  (SELECT COUNT(*) FROM table3) as table3count;
+-------------+-------------+-------------+
| table1count | table2count | table3count |
+-------------+-------------+-------------+
|           0 |           0 |           0 |
+-------------+-------------+-------------+
```

最上位の table1 を吹き飛ばすと全て吹き飛んでいるのがわかります。

## 結論というか感想

「（個人的には） generated column はほぼ普通のカラムと同じように外部キー制約を活用できる」

都道府県コードと市区町村コードのように、「親子関係があり、かつ子キー値が親キー値を内包している」ときにいい感じに出来るかもしれません。
例えば東京都コードは「13」で、千代田区コード「13101」ですが、これのテーブル定義、いつも悩むんですよ。。。

- t_pref
  - pref_cd: 13
  - pref_name: 東京都
- t_city
  - city_cd: 13101
  - city_name: 千代田区

という構造だと外部キー制約が貼れないのでつらみがある。

- t_pref
  - pref_cd: 13
  - pref_name: 東京都
- t_city
  - pref_cd: 13
  - city_cd: 101
  - city_name: 千代田区

だと外部キー制約は貼れるけど、13101 という値を得るのに一手間必要だし、t_city を参照したいトランザクションテーブルで外部キーのために2カラム必要になる。

- t_pref
  - pref_cd: 13
  - pref_name: 東京都
- t_city
  - pref_cd: 13
  - city_cd: 13101
  - city_name: 千代田区

だと外部キーは貼れるし、13101 という値も直に得られるけど、13 を冗長に持ってることになる。

そこで2番目の方法で CONCAT(pref_cd, city_cd) (必要に応じて LPAD も)で generated column すればいい。
city_cd が必要なテーブルではその generated colum に対して外部キーを貼れば単一カラムで済む。

まぁ city_cd を持つようなテーブルは得てして pref_cd も持つので意味ないんだけどね…。まぁ例ということで。