web-dev-qa-db-fra.com

Relation une à "zéro ou une" "

J'ai les exigences suivantes:

  • Chaque transaction a l'un des types suivants; débit, crédit, dépôt ou retrait.

  • Les transactions de débit ou de crédit doivent avoir un enregistrement de facturation lié et aucun enregistrement de compte bancaire.

  • Les transactions de dépôt ou de retrait doivent avoir un enregistrement de compte bancaire lié et aucun enregistrement de facturation.


Actuellement, ma conception de base est comme ceci:

Rough Database Design

Mes solutions actuelles sont:

  1. Avoir unedeux clés étrangères nullables aux tables de facturation et de compte bancaire dans la table de transaction. Cependant, je pense qu'une colonne de clé étrangère nullable n'est pas une bonne conception.
  2. Avoir une clé étrangère dans les tables de facturation et de compte bancaire qui pointent sur la table des transactions. Cependant, il semble plus difficile d'interroger et de modéliser une relation unique (pas une relation à une à facultative, je veux).

Quelle approche que j'ai décrite est une meilleure solution? Ou y a-t-il une autre façon de rencontrer mes contraintes encore mieux?

6

Permettez-moi de suggérer une troisième alternative: vous pouvez utiliser déclencheurs Pour vérifier vos règles d'entreprise et échouer l'insertion/la mise à jour de la même manière une contrainte de clé étrangère.

Pour votre scénario, j'ai configuré quelque chose comme ceci:

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

J'ai utilisé Sqlstate 45000 qui est un état générique pour des exceptions définies par l'utilisateur. Vous pouvez personnaliser cela et définir un message d'erreur approprié.

Vous auriez également besoin de créer un déclencheur avant la mise à jour qui fait un chèque similaire.

Il y aura une pénalité de performance pour abuser de déclencheurs de cette façon. Chaque insertion/mise à jour exécutera 2 sélectionne sur l'autre table. Vous devez optimiser les sélections pour être aussi vite que possible. Sur les bases de données de transaction élevées, il pourrait s'agir d'une mauvaise solution. Donc, si vous choisissez de prendre cette route, veuillez tester l'impact avant de l'exécuter sur la production.

1
socaire

Vous n'avez pas de table de transaction, vous avez une table [BankAccountTransaction] et une table [CreditTransaction].

Oui, divisez votre transaction dans deux tables, on a un FK à [BankAccount] et l'autre a obtenu un FK à [Facture].

Si le besoin se pose, vous pouvez créer une table "parent" [transaction] et l'utiliser pour intermédiaire la relation entre les deux tables ci-dessus à une table ne voyant pas la différence entre eux. Ou si cela important uniquement pour signaler comme des requêtes, vous pouvez créer une vue où vous utilisez facilement un syndicat.

                        +---------------------+                +---------------+
                        |                     |                |               |
        +---------------+  CreditTransaction  +---------------->    Invoice    |
+-------v------+        |                     |                |               |
|              |        +---------------------+                +---------------+
|              |
| Transaction  |
|              |
|              |          +------------------------+       +----------------+
+-------^------+          |                        |       |                |
        |                 | BankAccountTransaction |       | BankAccount    |
        +-----------------+                        +------->                |
                          |                        |       |                |
                          +------------------------+       +----------------+
0
jean

Bien que de très nombreuses colonnes nullables soient normalement un signe de mauvaise conception, dans certains cas, il est parfaitement légitime de les avoir et utiliser la base de données CONSTRAINTS pour vous assurer que les relations 0-1 et vos règles de gestion sont conservées. Ce serait votre solution n ° 1. Votre solution n ° 2 n'est pas facilement aidée par la base de données, vous pouvez éventuellement avoir une transaction sans les lignes correspondantes dans les deux invoices et bank_accounts les tables.

Supposons un instant que vous êtes pas Utilisation de MySQL1 (au moins, à la version 5.7).

Supposons que vous utilisiez une autre base de données qui effectue réellement les chèques.2, il serait logique d'utiliser un schéma comme le suivant, avec un invoice_id et une bank_account_id colonnes et les contraintes nécessaires qui garantissent qu'ils sont REFERENCEing les lignes appropriées dans les tables appropriées (ce que vous appelez liens), et le CHECKS Assurez-vous que les propres appropriés apparaissent et ceux qui ne correspondent pas ne sont pas là:

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) 
) ;

Dans cet esprit, et les données suivantes ...

-- 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')
;

... vous pouvez avoir un légit 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) ;

... et quelques-uns illégaux (être rejeté par la base de données)

-- 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) ;
[. ____
-- 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) ;
[. ____

(et toutes les autres combinaisons)

Un choix attentif de noms pour les contraintes peut aider beaucoup à déboguer des inserts ou des mises à jour erronées. Si vous avez besoin d'une vitesse maximale, toutes les contraintes peuvent être réduites à une seule expression de chèques. Je vais normalement essayer d'avoir la base de données Aide-moi, et gardez-le simple (4 noms faciles et 4 expressions faciles à lire, au lieu d'une seule).

Vous pouvez trouver toute la configuration sur dbfiddle ICI


1C'est la raison pourquoi:

Malheureusement, MySQL n'aide pas beaucoup, car à partir de Manuel 5.7 de MySQL sur la table Create

[~ # ~] vérifier [~ # ~ ~]

La clause de contrôle est analysée mais ignorée par tous les moteurs de stockage. Voir la section 1.8.2.3, "Différences de clés étrangères".

2J'ai utilisé PostgreSQL. Avec certaines variations de syntaxe, cela fonctionnerait également avec SQL Server ou Oracle.

0
joanolo