次の要件があります。
すべてのトランザクションには、次のいずれかのタイプがあります。デビット、クレジット、入金、または引き出し。
デビットまたはクレジット取引には、リンクされた請求書レコードが必要であり、銀行口座レコードはありません。
預金または引き出し取引には、リンクされた銀行口座レコードがあり、請求書レコードがない必要があります。
現在、私の基本的なデザインは次のとおりです。
私の現在の解決策は:
私が概説したどのアプローチがより良い解決策ですか?それとも、より良い私の制約を満たす別の方法はありますか?
3番目の代替案を提案させてください。triggersを使用してビジネスルールをチェックし、外部キー制約と同じように挿入/更新を失敗させることができます。
あなたのシナリオでは、私はこのようなものをセットアップしました:
CREATE TRIGGER Transaction_BEFORE_INSERT BEFORE INSERT ON Transaction_Table FOR EACH ROW
BEGIN
-- deposit, withdraw must have a bank account and no invoice
if NEW.`Type` in ('deposit','withdraw') then
if not(select count(*) from Account_Table where id = NEW.id)
or (select count(*) from Invoice_Table where id = NEW.id)
then
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Error message';
end if;
-- credit, debit must have an invoice and no bank account
elseif NEW.`Type` in ('credit','debit') then
if not(select count(*) from Invoice_Table where id = NEW.id)
or (select count(*) from Account_Table where id = NEW.id)
then
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Another error message';
end if;
end if;
END
ユーザー定義の例外の一般的な状態であるSQLSTATE 45000を使用しました。これをカスタマイズして、適切なエラーメッセージを設定することもできます。
同様のチェックを行うBEFORE UPDATEトリガーを作成する必要もあります。
この方法でトリガーを悪用すると、パフォーマンスが低下します。各挿入/更新は、他のテーブルで2つの選択を実行します。できるだけ速くなるように選択を最適化する必要があります。トランザクション数の多いデータベースでは、悪い解決策になる可能性があります。したがって、この方法を選択する場合は、本番環境で実行する前に影響をテストしてください。
トランザクションテーブルはなく、[BankAccountTransaction]テーブルと[CreditTransaction]テーブルがあります。
はい、トランザクションを2つのテーブルに分割します。1つは[BankAccount]へのFK、もう1つは[請求書]へのFKです。
必要に応じて、「親」[トランザクション]テーブルを作成し、それを使用して上記の2つのテーブル間の関係をテーブルに仲介することができます。これらの違いはわかりません。または、クエリと同様にレポートすることだけが重要な場合は、UNIONを簡単に使用できるビューを作成できます。
+---------------------+ +---------------+
| | | |
+---------------+ CreditTransaction +----------------> Invoice |
+-------v------+ | | | |
| | +---------------------+ +---------------+
| |
| Transaction |
| |
| | +------------------------+ +----------------+
+-------^------+ | | | |
| | BankAccountTransaction | | BankAccount |
+-----------------+ +-------> |
| | | |
+------------------------+ +----------------+
通常、非常に多くのNULL可能列は設計が不適切であることを示していますが、場合によっては、これらの列を用意し、データベースCONSTRAINTS
を使用して0-1の関係とビジネスルールが維持されるようにすることもできます。これがあなたの解決策#1です。ソリューション#2はデータベースによって簡単に支援されないため、invoices
とbank_accounts
の両方のテーブルに対応する行がない場合、最終的に1つのトランザクションが発生する可能性があります。
少しの間、MySQLを使用していないと仮定しましょう1 (少なくとも、バージョン5.7以降)。
実際にチェックを実行する別のデータベースを使用しているとしましょう2、1つのinvoice_id
列と1つのbank_account_id
列、および適切な行をREFERENCE
ingすることを保証する必要な制約を含む、次のようなスキーマを使用することは理にかなっています。適切なテーブル(あなたが呼ぶものlinks)とCHECKS
は、適切なテーブルが表示され、対応していないものがそこにないことを確認します。
CREATE TYPE transaction_type AS ENUM
('debit', 'credit', 'deposit', 'withdraw') ;
-- Note: this could be a table with four values (and probably four ids)
CREATE TABLE invoices
(
invoice_id integer /* serial */ PRIMARY KEY,
other_data text
) ;
CREATE TABLE bank_accounts
(
bank_account_id integer /* serial */ PRIMARY KEY,
name text,
other_data text
) ;
CREATE TABLE transactions
(
transaction_id integer /* serial */ PRIMARY KEY,
type transaction_type,
nominal decimal(10, 2),
invoice_id integer REFERENCES invoices(invoice_id) ON UPDATE CASCADE ON DELETE RESTRICT,
bank_account_id integer REFERENCES bank_accounts(bank_account_id) ON UPDATE CASCADE ON DELETE RESTRICT,
-- Constraints for your business rules
CONSTRAINT chk_debit_and_credit_must_have_bank_account
CHECK (case when type in ('debit','credit') then
bank_account_id IS NOT NULL
else true end),
CONSTRAINT chk_debit_and_credit_must_not_have_invoice
CHECK(case when type in ('debit','credit') then
invoice_id IS NULL
else true end),
CONSTRAINT chk_deposit_and_withdraw_must_have_invoice
CHECK(case when type in ('deposit','withdraw') then
invoice_id IS NOT NULL
else true end),
CONSTRAINT chk_deposit_and_withdraw_must_not_have_bank_account
CHECK(case when type in ('deposit','withdraw') then
bank_account_id IS NULL
else true end)
) ;
これを念頭に置いて、次のデータ...
-- Adding two invoices
INSERT INTO invoices
(invoice_id, other_data)
VALUES
(1, 'data for invoice 1'),
(2, 'data for invoice 2')
;
-- Adding two bank accounts
INSERT INTO bank_accounts
(bank_account_id, other_data)
VALUES
(1000, 'Bank account 1000'),
(1001, 'Bank account 1001')
;
... 1つの合法性を持つことができますINSERT
-- Good credit and debit
INSERT INTO transactions
(transaction_id, type, nominal, invoice_id, bank_account_id)
VALUES
(2000, 'credit', 1000.00, NULL, 1000),
(2001, 'debit', 900.00, NULL, 1000) ;
...およびいくつかの違法なもの(データベースによって拒否されている)
-- Bad credit, it's got invoice
INSERT INTO transactions
(transaction_id, type, nominal, invoice_id, bank_account_id)
VALUES
(2002, 'credit', 1000.00, 1, 1000) ;
エラー:リレーション「トランザクション」の新しい行がチェック制約「chk_debit_and_credit_must_not_have_invoice」に違反しています 詳細:失敗した行には(2002、クレジット、1000.00、1、1000)が含まれています。
-- Bad credit, it's got not bank_account_id
INSERT INTO transactions
(transaction_id, type, nominal, invoice_id, bank_account_id)
VALUES
(2003, 'credit', 1000.00, NULL, NULL) ;
エラー:リレーション「トランザクション」の新しい行はチェック制約「chk_debit_and_credit_must_have_bank_account」に違反しています 詳細:失敗した行には(2003、クレジット、1000.00、null、null)が含まれています。
(および他のすべての組み合わせ)
制約の名前を注意深く選択すると、誤った挿入や更新のデバッグに役立ちます。最大速度が必要な場合、すべての制約を1つのチェック式に減らすことができます。私は通常、データベースが私を助けて、シンプルに保つようにします(1つの式ではなく、4つの簡単な名前と4つの読みやすい式)。
すべての設定はdbfiddle ここにあります
1これが理由です。
残念ながら、 MySQLのCREATE TABLE に関するMySQL 5.7マニュアルから、MySQLはあまり役に立ちません。
[〜#〜]チェック[〜#〜]
CHECK句は解析されますが、すべてのストレージエンジンによって無視されます。セクション1.8.2.3「外部キーの違い」を参照してください。
2私はPostgreSQLを使用しました。いくつかの構文のバリエーションがあれば、これはSQL ServerまたはOracleでも機能します