2012-07-05

MySQL Workbench の EER モデリングツールでリレーションシップ検証 2 (外部キー制約の種類と動作)

前回の記事では、リレーションシップ構築時の idendifying (依存型)および non identifying (非依存型) の考え方と設計方法について述べましたが、今回は外部キーで設定可能な制約の種類と動作について説明します。


【外部キー制約とは】

子テーブル側の外部キーと、親テーブルの主キーの参照整合性を維持するために、子テーブル側の外部キーの動作を制限するものです。

たとえば、前回のブランドと取扱商品との関係性では、取扱商品テーブル側に不明な外部キー([ブランドID])が存在しないように制御したり、ブランドテーブル側から特定のブランドを削除した場合、そのブランドに関連する商品を取扱商品から自動的に連動して削除させたりといった振る舞い(制約)を設定します。


MySQL Workbench の EER モデリングでは、下図のように Foreign Key タブ上の Foreign Key Option を選択するだけで外部キー制約の設定が行えます。



【On Update と On Delete】

外部キー制約のオプションは、更新時(On Update)、削除時(On Delete)に大別されます。

これらは、親テーブル側(上図ではブランド)側の動作を示しており、親テーブル側のレコードが更新されたり(On Update)、削除されたり(On Delete)したときに、子テーブル(上図では取扱商品)の関連レコードの扱いを定義するという意味です。
そして、実際の関連レコードの扱いは、上図の赤囲みのドロップダウンリストから選択します。


【外部キー制約の種類】

ドロップダウンリストから選択可能な外部キー制約は次のとおりです。

RESTRICT/NO ACTION
子テーブルに関連レコードがあったら、親レコードの更新/削除を許可しない。

NO ACTION はステートメントおよびすべてのトリガーが起動された後に子テーブルの値がまだ有効であれば変更は許可される。
これに対し、RESTRICT は、子テーブルに関連レコードが存在する場合、変更は絶対に許可されない。
RDBMS ではこのような厳密な動作の違いがあるものもあるが、MySQL では RESTRICT と NO ACTION の挙動は同じ。


たとえば、取扱商品(子テーブル側)の登録を始めたら、ブランド名(親テーブル側)の変更をさせないようにする場合がこれに相当します。

CASCADE
親テーブルのレコードを更新/削除したら、子テーブルのレコードも更新/削除する。
厳密には、親テーブルの主キーの値を更新したら、子テーブルの外部キーの値も同じ値で更新され、親テーブルのレコードを削除したら、そのレコードに関連する子テーブルのレコードも連動して削除される。

たとえば、特定のブランド(親テーブル側)を削除したら、そのブランドを持つすべての取扱商品(子テーブル側)を削除するようにする場合がこれに相当します。

SET NULL
親テーブルのレコードを更新/削除したら、子テーブルの外部キーの値を NULL に設定する。
これにより、当該レコードの親子関係が解除されることになる。

たとえば、ブランド(親テーブル側)を削除したとしても、取扱商品(子テーブル側)は将来の管理用にデッドストックとして残しておく場合がこれに相当します。


親テーブルの主キーを変更した場合、および親レコードを削除した場合の外部キー制約の振る舞いを表にすると、以下のようになります。


動作条件挙動親(子なし)親(子あり)
On UpdateRESTRICT/NO ACTION親主キー更新可親主キー更新不可変化なし
CASCADE親主キー更新可親主キー更新可外部キー連動更新
SET NULL親主キー更新可親主キー更新可外部キーNULL
On DeleteRESTRICT/NO ACTION親レコード削除可親レコード削除不可変化なし
CASCADE親レコード削除可親レコード削除可子レコード連動削除
SET NULL親レコード削除可親レコード削除可外部キーNULL


【SQL による外部キー制約の例】


前述の外部キー制約のパターンのうちのいくつかを、実際の SQL による定義でいくつか確認してみましょう。太字が外部キーの設定と制約を定義している部分です。特に赤字の部分に注目してください。
挙動については、前項のテーブルをご覧ください。



例1: 
On Update: NO ACTION
On Delete: CASCADE

--
-- Table structure for table `取扱商品`
--

DROP TABLE IF EXISTS `取扱商品`;
SET @saved_cs_client     = @@character_set_client;
SET character_set_client = utf8;
CREATE TABLE `取扱商品` (
  `ブランドID` int(11) DEFAULT NULL,
  `商品ID` int(11) NOT NULL AUTO_INCREMENT,
  `商品名` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
  PRIMARY KEY (`商品ID`),
  KEY `fk_取扱商品_ブランド` (`ブランドID`),
  CONSTRAINT `fk_取扱商品_ブランド` FOREIGN KEY (`ブランドID`) REFERENCES `ブランド` (`ブランドID`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;
SET character_set_client = @saved_cs_client;



例2: 
On Update: CASCADE
On Delete: SET NULL


--
-- Table structure for table `取扱商品`
--


DROP TABLE IF EXISTS `取扱商品`;
SET @saved_cs_client     = @@character_set_client;
SET character_set_client = utf8;
CREATE TABLE `取扱商品` (
  `ブランドID` int(11) DEFAULT NULL,
  `商品ID` int(11) NOT NULL AUTO_INCREMENT,
  `商品名` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
  PRIMARY KEY (`商品ID`),
  KEY `fk_取扱商品_ブランド` (`ブランドID`),
  CONSTRAINT `fk_取扱商品_ブランド` FOREIGN KEY (`ブランドID`) REFERENCES `ブランド` (`ブランドID`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=latin1;
SET character_set_client = @saved_cs_client;


2012-07-04

MySQL Workbench の EER モデリングツールでリレーションシップ検証 1 (Identifying と Non identifying)

MySQL Workbench の便利な機能に EER モデリングツールがあります。


MySQL Workbench の EER モデリング例 


EER モデリングツールを使うことによって、データベースの基本設計を行い、それを使って MySQL  データベース本体を生成したり(フォワードエンジニアリング)、既存の MySQL データベースを EER モデリングツールに読み込ませて(リバースエンジニアリング)設計内容を修正した後で再び MySQL データベースに同期(シンクロナイジング)させたりすることができます。

EER モデリングツールの魅力は、何といってもデータベース設計を視覚的に行えるところにあることでしょう。
テーブル定義、列定義はもちろんのこと、テーブルオブジェクトの配置、リレーションシップの設定などもマウスクリックや簡単な入力作業である程度の設計が可能です。

MySQL Workbench は、MySQL の公式サイトからダウンロードできます。

ダウンロード先:MySQL Workbench 5.2


今回より、MySQL Workbench の EER モデリング機能で設定可能なリレーションシップの種類と、その動作の違いについて数回に分けて説明します。

リレーションシップの種類には、一対一、一対多、多対多、自己リレーションなどがありますが、それらの基本的な概念は他のサイトでも多く触れられているため、本記事では省略します。


【Identifying と Non identifying】

EER でリレーションシップ設定を行い、外部キーの設定情報を確認したときに以下のようなチェックボックスが気になった方もいらっしゃるのではないでしょうか。



そこで、今回は、上記のリレーションの外部キー属性となる、identifying と non identifying の違いについて説明します。


Identifying relationship (依存リレーションシップ)--- 子テーブルの外部キーの一部に親テーブルの主キーが組み込まれた形となり、親テーブルなしでは成り立たない関係。


たとえば、ブランド契約で販売する商品が、撤退時に商品の取扱いも同時に終了するように、ブランドテーブルと取扱い商品テーブルを関連づける場合、[商品ID] および[ブランドID] の組み合わせで外部キーを作り、[ブランドID]  が存在しなければその商品は存在し得ないような厳密な設計を行います。



本来、子テーブル側の主キーは一つ([商品ID])ですが、依存型リレーションシップの場合は親テーブル側のIDを識別するための列([ブランドID])も主キー扱いとなりますので、上図のように子テーブルの主キーは二つとなります(どちらも欠かせません)。
 
 EERモデリングでは、identifying リレーションのコネクタは、上図のように実線で表現されます。

 この場合、外部キー設定がされている取扱商品テーブル構成をダンプ(SQL表記)させると、以下のようになります。
 太字表記の部分が今回のチェック項目になりますが、特に赤字の部分に注目してください。[商品ID] および [ブランドID] の両方が主キー となっています。

--
-- Table structure for table `取扱商品`
--

DROP TABLE IF EXISTS `取扱商品`;
SET @saved_cs_client     = @@character_set_client;
SET character_set_client = utf8;
CREATE TABLE `取扱商品` (
  `ブランドID` int(11) NOT NULL,
  `商品ID` int(11) NOT NULL AUTO_INCREMENT,
  `商品名` varchar(45) CHARACTER SET utf8 DEFAULT NULL,
  PRIMARY KEY (`商品ID`,`ブランドID`),
  KEY `fk_取扱商品_ブランド` (`ブランドID`),
  CONSTRAINT `fk_取扱商品_ブランド` FOREIGN KEY (`ブランドID`) REFERENCES `ブランド` (`ブランドID`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
SET character_set_client = @saved_cs_client;


Non identifying relationship(非依存リレーションシップ)--- 親テーブルの主キーと子テーブルの外部キーでリレーションシップを構築するが、子テーブルに外部キーとは独立した主キーを持っている関係。


前述のモデルの応用例として、ブランド管理が廃止され商品だけが残った場合にも、在庫商品を処分(販売)できるように、ブランドを問わず商品を管理して扱う場合は、親テーブルが削除された場合にも、子テーブルの商品だけは残して管理できるように設計します。





EERモデリングでは、non-identifying リレーションのコネクタは、上図のように破線で表現されます。


この場合、外部キー設定がされている取扱商品テーブル構成をダンプ(SQL表記)させると、以下のようになります。
太字表記の部分が今回のチェック項目になりますが、特に赤字の部分に注目してください。[商品ID] のみが主キー となっています。
--
-- Table structure for table `取扱商品`
--


DROP TABLE IF EXISTS `取扱商品`;
SET @saved_cs_client     = @@character_set_client;
SET character_set_client = utf8;
CREATE TABLE `取扱商品` (
  `ブランドID` int(11) NOT NULL,
  `商品ID` int(11) NOT NULL AUTO_INCREMENT,
  `商品名` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`商品ID`),
  KEY `fk_取扱商品_ブランド` (`ブランドID`),
  CONSTRAINT `fk_取扱商品_ブランド` FOREIGN KEY (`ブランドID`) REFERENCES `ブランド` (`ブランドID`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SET character_set_client = @saved_cs_client;



【関連リンク】
MySQL Workbench 基礎講習コース


2012-06-16

『売上猫くん on MySQL』開発日記 - 番外25 - 行ロックについて

小社主催の「FileMaker+MySQL開発の講習」の受講者の方から「FileMakerとMySQLのシステムで、FileMaker(単体システム)のような行ロックってできないの?」との質問を講習前に頂いた。
FileMaker単体のシステムでは、ユーザがあるレコードを編集中に他のユーザが同レコードを編集しようとしても「他のユーザが編集中」旨が表示され、他のユーザは先行するユーザが編集を終了するまで編集することができなくなる。質問の趣旨はこれをFileMaker/MySQLのシステムでも実現したいということ。 
FileMaker標準機能では、FileMaker/MySQL 環境下で排他的行ロックはできない。 が、方法としては2つある。一つはMySQLのロック機能(select for update)を使用する方法(MySQLの排他的行ロック)、もう一つはMySQLテーブルにロックフラグを用意し、最初に編集を開始したユーザがそのフラグに自分のIDを書き込み、後続の他ユーザの編集を禁じるようにごりごりスクリプトで制御方法(FileMaker単体の行ロックと似ているという意味で、FileMakerライクなロック)。 以下はこの2つのロックの実装方法について。

1.MySQLの排他的行ロック
まず、MySQLの排他的行ロック機能を使うと、FileMakerはどのように反応するのか、試してみる。

MySQLコマンドラインで、

  begin;
  select 売上No from 売上ヘッダ where 売上No=1 for update;


を実行する。 次にFileMakerの売上画面で売上No=1のレコードを開いてみると、何事もなく表示される。次にこのレコードの任意のフィールド値を変更してみると普通に変更できる。 さらに、レコードをCommitすると… 出た、必殺のコーヒーマーク! これがMySQLの排他的行ロックである。 ここで、MySQLコマンドラインで、


 commit;


を実行すると、コヒーマークは消えて、レコードがCommitされた。 MySQLの排他的行ロック(select for update)は他ユーザがレコードをselectすることを妨げず、よってFileMakerのレイアウト上にも表示される。 また一旦表示されたレコードは、他のユーザが編集していたとしても、後発ユーザの編集を妨げるよなFileMaker的ロックは行われない。 しかし、いざCommitを実行しようとすると、コーヒーマックが出て、select for update を実行したユーザのトランザクションがCommitかRollbackされるまで、コヒーマークが出たまま、つまりロックされた状態となる。
【FileMakerとMySQLコマンドライン】
select ~ for update 後にFMで編集→確定ボタンすると、コーヒーカップが… 
注:
通常、ロックが発生から60秒後に、「[MySQL][ODBC 5.1 Driver][mysqld-5.1.58-community]Lock wait timeout exceeded; try restarting transaction」というエラーが返る。レコード確定ステップ実行後に後処理がある場合は、このエラーをトラップして処理を中止するなど適切な処理を行わないと、システムが暴走する可能性があるので要注意
FileMakerからも「SQLを実行」スクリプトステップに上記内容を直書きしたり、ストアドプロシージャをCALLすれば、ロックやトランザクションを利用できる。試しに


BEGIN
start transaction;
 select * from 売上ヘッダ where 売上No=1 for update;
 select sleep(60);
 rollback;
END;


というストアドプロシージャを作成して、「SQLを実行」ステップからこれをCALLしてみる。 問題なく実行でき、結果、60秒の間そのレコードは更新できなくなる。しかし 「SQLを実行」 ステップはクライアントPCにDSN登録がないと実行できない、という問題があるため、FileMakerからMySQLトリガを発生させてストアドを実行したい(この方法については『売上猫くん on MySQL』開発日記 - 3 - FMからストアド、トリガを参照)。 ということでトリガから上記のストアドを実行しようとすると、「 Not allowed to return a result set from a trigger」というエラーが出るので、レコードセットが返らないように下記のようにストアドを書きなおした。

BEGIN
    declare temp int;
    start transaction;
        select Count(*) into temp from 売上ヘッダ where  売上No =1 for update;
        select sleep (60) into temp;
        update appliasons set ret="OK" where ID=appId;
    commit;
END
ところが残念なことが重なる。今度は「Explicit or implicit commit is not allowed in stored function or trigger.」というエラーが返ってくる。 これでMySQLのトリガではトランザクションが使用できないことがわかった。まえに小生が書いた記事『売上猫くん on MySQL』開発日記 - 番外12 - トリガの制約に、この件をちらっと書いていた(恥)。 
結論としては、ストアドを直接実行すればMySQLのロックを実行できるが、トリガを介したストアド実行によるロックは不可、ということになる。

尚、FM12の新関数ExecuteSQL関数では、MySQLのロックは利用できない(つか、この関数、相当中途半端な上、意味不明なクエリを発する。書いた通りの動きをしないこともあるし…。この辺りは後日機会があれば)。


2.FileMakerライクなロック
次にFileMakerライクなロック。これは開発者が自分でゴリゴリ書かなくてはならないのでかなり面倒だ。 以下は小社のFileMaker/MySQL講習テキストから実装方法を引用する。
【実装】
“編集”ボタンを用意し、レコードを変更する必要があるユーザには“編集”ボタンを実行して「編集モード」にすることを義務付ける。編集モードでなければレコードの変更は実行できないよう制御するとともに、“編集”ボタン実行時には、当該レコードのロックフラグフィールド(cntLockId)にユーザ固有のIDをセット。このフラグが解除されない間は他ユーザが当該レコードの変更を行えないように制御。ユーザがレコードの更新を終えCommitした瞬間にこのロックフラグフィールドを解除するように設計する。
名称
種類
仕様
cntLockId
フィールド
(
テキスト)
編集ボタンを実行するとこのフィールドにユーザ固有のID=Get ( 持続 ID )が入力され、このIDを持たない他ユーザは編集不可となる。 
cntLockTiime
フィールド
(
タイムスタンプ)
編集ボタン実行時に[cntLockId]とともに本フィールドにはサーバのタイムスタンプ(現日時)が入力される。 このタイムスタンプによりデッドロックになっていないかを判断し、管理者は適切な処理をおこなう。
cntLockAC
フィールド
(
テキスト)
必要に応じて作成。編集ボタン実行時にGet ( アカウント名 )が入力される。 これを利用すれば、編集実行者を画面上に表示することができる。
(ユーザが手入力可能なフィールド)
フィールド
OnObjectEnterスクリプトトリガを用いて、[cntLockId<> Get ( 持続 ID )の場合は、フィールドに入れない(フィールド値を変更できない)ように制御。
編集
ボタン
上記のcntLockId/ cntLockTiime/ cntLockAC参照。左記3フィールドに値を入力後にレコードを確定する。これによりそのレコードは他ユーザによる編集が不可となる。
確定
ボタン
上記のcntLockId/ cntLockTiime/ cntLockACの値をクリア(ロックを解除)後、レコードを確定する。
復帰
ボタン
デッドロックしないよう([cntLockId]の値が残らない)ように制御。
他のボタン
ボタン
削除ボタン、削ボタン(売上明細削除用)など、当該レコードを変更する可能性があるボタン、スクリプトは[cntLockId]に他ユーザのIDがある場合は、実行出来ないように制御する。

【編集ボタンによるFileMakerライクなロック】

[cntLockId]と同じGet(持続ID)を持つユーザのみが編集可となり、確定ボタンが完了すると[cntLockId][cntLockTime][cntLockAC]はnullとなり、他のユーザも編集可能となる。


注:
サーバのRemote Desktop Service を介して複数人でFileMakerを使用する場合、Get(持続ID)は重複すると思われる。この場合、他の方法で個別IDを取得すること。

(土屋)