Kann diese Geschäftslogik durch eine bedingte Datenbankbeschränkung erzwungen werden?

7

Ich versuche, die Geschäftslogik einer Intranet-C # -Webanwendung in der Datenbank zu duplizieren, damit andere Datenbanken darauf zugreifen und nach denselben Regeln arbeiten können. Diese "Regel" scheint ohne Hacks schwierig zu implementieren zu sein.

CREATE TABLE CASE_STAGE
(
  ID                        NUMBER(9)           PRIMARY KEY NOT NULL, 
  STAGE_ID                  NUMBER(9)           NOT NULL,
  CASE_PHASE_ID             NUMBER(9)           NOT NULL,
  DATE_CREATED              TIMESTAMP(6)        DEFAULT CURRENT_TIMESTAMP     NOT NULL,
  END_REASON_ID             NUMBER(9),
  PREVIOUS_CASE_STAGE_ID    NUMBER(9),
  "CURRENT"                 NUMBER(1)           NOT NULL,
  DATE_CLOSED               TIMESTAMP(6)        DEFAULT NULL
);

und

CREATE TABLE CASE_RECOMMENDATION
(
  CASE_ID                   NUMBER(9)           NOT NULL,
  RECOMMENDATION_ID         NUMBER(9)           NOT NULL,
  "ORDER"                   NUMBER(9)           NOT NULL,
  DATE_CREATED              TIMESTAMP(6)        DEFAULT CURRENT_TIMESTAMP     NOT NULL,
  CASE_STAGE_ID             NUMBER(9)           NOT NULL
);

ALTER TABLE CASE_RECOMMENDATION ADD (
  CONSTRAINT SYS_C00000
 PRIMARY KEY
 (CASE_ID, RECOMMENDATION_ID));

Die Geschäftslogik kann wie folgt zusammengefasst werden

When Inserting into CASE_STAGE
If CASE_STAGE.STAGE_ID = 1646
THEN
 CASE_STAGE.PREVIOUS_STAGE_ID must be found in CASE_RECOMMENDATION.CASE_STAGE_ID

Kann diese Logik in einer Check-Einschränkung enthalten sein oder ist ein hässlicher Trigger der einzige Weg?

Bearbeiten:

  • Für alle Werte von CASE_STAGE.STAGE_ID muss der Wert für PREVIOUS_STAGE_ID in CASE_STAGE.ID gefunden werden
  • Die Anwendung erlaubt keine Löschungen aus CASE_RECOMMENDATION, wenn sie nicht mehr CURRENT ist (dh wenn der Wert von CASE_STAGE.CURRENT 0 ist, ist diese Stufe geschlossen und kann nicht mehr geändert werden, wenn = 1, ist dies die Stufe oder Zeile, die aktiv ist und kann jetzt geändert werden.)

Bearbeiten: Die Verwendung all der hervorragenden Ideen und Kommentare hier ist eine funktionierende Lösung für dieses Problem

CREATE MATERIALIZED VIEW LOG ON CASE_STAGE
TABLESPACE USERS
STORAGE    (
            BUFFER_POOL      DEFAULT
           )
NOCACHE
LOGGING
NOPARALLEL
WITH ROWID;

CREATE MATERIALIZED VIEW LOG ON CASE_RECOMMENDATION
TABLESPACE USERS
STORAGE    (
            BUFFER_POOL      DEFAULT
           )
NOCACHE
LOGGING
NOPARALLEL
WITH ROWID;

CREATE MATERIALIZED VIEW CASE_RECOMMENDATION_MV REFRESH FAST ON COMMIT AS
  SELECT
         cr.ROWID cr_rowid, --necessary for fast refresh
         cs.ROWID cs_rowid, --necessary for fast refresh
         cr.case_id,
         cs.stage_id,
         cr.recommendation_id
         cr.case_stage_id,
         cs.previous_case_stage_id
  FROM   CASE_RECOMMENDATION cr,
         case_stage cs
  WHERE  cs.previous_case_stage_id = cr.case_stage_id (+)
  AND CS.PREVIOUS_CASE_STAGE_ID IS NOT NULL
  AND EXTRACT (YEAR FROM CS.DATE_CREATED) > 2010 --covers non conforming legacy data
  AND CR.RECOMMENDATION_ID IS NULL
  AND cs.stage_id =1646;  
--this last line excludes everything but problem cases due to the outer join

ALTER TABLE CASE_RECOMMENDATION_MV ADD CONSTRAINT CASE_RECOMMENDATION_ck CHECK (
    (previous_case_stage_id IS NOT NULL AND case_stage_id IS NOT NULL)
);

Beim Einfügen einer 1646-Stufe unter Verwendung vorhandener Pakete ohne Empfehlung war der Fehler

ORA-12008: error in materialized view refresh path
ORA-02290: check constraint (APPBASE.CASE_RECOMMENDATION_MV_C01) violated
ORA-06512: at line 49

Job erledigt! Nicht wofür eine materialisierte Ansicht gedacht war, sondern besser als ein Auslöser.

kevinsky
quelle
Ist CASE_RECOMMENDATION.CASE_STAGE_IDeinzigartig?
Vincent Malgrat
Leider nicht. Eine Fallphase kann eine oder mehrere Empfehlungen in mehreren Phasen enthalten. Es kann null bis viele Stufen mit CASE_STAGE.STAGE_ID = 1646
kevinsky
Sie können die referenzielle Integrität leider nicht mit nicht eindeutigen Spalten verwenden.
Vincent Malgrat
Was ist das Besondere daran 1646? Ist es eine Option, das Design zu ändern (Tabelle in zwei Teile zu teilen)?
Ypercubeᵀᴹ
Und wie streng ist das "beim Einfügen in" zu interpretieren? Ist es gültig, eine zulässige Einfügung vorzunehmen und dann die referenzierte Zeile aus dem übergeordneten Element zu löschen?
Erwin Smout

Antworten:

4

Wenn Sie komplexe Einschränkungen haben, die Sie "unsichtbar" in der Datenbank anwenden möchten, können Sie dies tun, indem Sie eine materialisierte Ansicht erstellen und dann Einschränkungen darauf anwenden.

In diesem Fall können Sie es ein MV-Außenverbindungs Verwendung CASE_RECOMMENDATION.CASE_STAGE_IDzu CASE_STAGE.PREVIOUS_CASE_STAGE_ID. Es sollte dann überprüft werden, ob keines von diesen null ist, wenn die CASE_STAGE.STAGE_ID = 1646, wie folgt:

--necessary for fast refresh
create materialized view log on case_stage with rowid;
create materialized view log on case_recommendation with rowid;

create materialized view mv refresh fast on commit as 
  select 
         cr.rowid cr_rowid, --necessary for fast refresh
         cs.rowid cs_rowid, --necessary for fast refresh
         cr.case_id,
         cr.recommendation_id,
         case when cs.stage_id = 1646 then
           'Y'
         else
           'N'
         end do_chk,
         cr.case_stage_id,
         cs.previous_case_stage_id
  from   CASE_RECOMMENDATION cr, 
         case_stage cs
  where  cs.previous_case_stage_id = cr.case_stage_id (+);

alter table mv add constraint mv_ck check (
    (do_chk = 'Y' and previous_case_stage_id is not null and case_stage_id is not null )
    or
    (do_chk = 'N')
);

insert into  CASE_STAGE values (1, 1, 1, sysdate, null, null, 1, null);

insert into CASE_RECOMMENDATION values (1, 1, 1, sysdate, 1);
commit;

insert into CASE_STAGE values (2, 1646, 1, sysdate, null, null, 1, null);

pro fails because previous_case_stage_id is null
commit;
SQL Error: ORA-12008: error in materialized view refresh path
ORA-02290: check constraint (CHRIS.MV_CK) violated
12008. 00000 -  "error in materialized view refresh path"

insert into CASE_STAGE values (2, 1646, 1, sysdate, null, 2, 1, null); 

pro fails because previous_case_stage_id doesn't exist in CASE_RECOMMENDATION'
commit;
SQL Error: ORA-12008: error in materialized view refresh path
ORA-02290: check constraint (CHRIS.MV_CK) violated
12008. 00000 -  "error in materialized view refresh path"

pro succeeds !
insert into CASE_STAGE values (2, 1646, 1, sysdate, null, 1, 1, null); 
commit;

pro we can't delete stuff from case recommendation now 
delete CASE_RECOMMENDATION;
commit;
SQL Error: ORA-12008: error in materialized view refresh path
ORA-02290: check constraint (CHRIS.MV_CK) violated
12008. 00000 -  "error in materialized view refresh path"

Die Prüfbedingung für das MV wird nur aufgerufen, wenn es aktualisiert wird. Damit dies erfolgreich funktioniert, müssen Sie sicherstellen, dass dies in COMMIT erfolgt. Dies erhöht Ihre Festschreibungszeitverarbeitung, sodass Sie Folgendes berücksichtigen müssen:

  • Sofern Ihre Datensätze nicht trivial klein sind, sollte das MV SCHNELL ERFRISCHEN. Es gibt einige Einschränkungen, wie Sie Ihr MV erstellen, um dies zu ermöglichen
  • Wenn eine Transaktion mehrere Einfügungen enthält, wird der Fehler nur beim Festschreiben ausgelöst, wodurch es möglicherweise schwieriger wird, die fehlerhafte Anweisung zu identifizieren
  • Wenn Sie eine hohe Anzahl gleichzeitiger Einfügungen haben, kann dies zu Problemen mit der Parallelität führen

Da diese Lösung die Einschränkungen in der SQL-Schicht implementiert, werden einige der in der prozeduralen Lösung diskutierten Parallelitätsprobleme überwunden.

AKTUALISIEREN

Wie von Vincent hervorgehoben, kann die Größe des MV reduziert werden, indem nur die Zeilen mit stage_id = 1646 eingeschlossen werden. Es ist möglicherweise möglich, die Abfrage neu zu schreiben, um keine Zeilen zu verbrauchen, aber ich kann mir nicht vorstellen, wie ich das richtig machen soll jetzt:

create materialized view mv refresh fast on commit as 
  select 
         cr.rowid cr_rowid, --necessary for fast refresh
         cs.rowid cs_rowid, --necessary for fast refresh
         cr.case_id,
         cr.recommendation_id,
         cr.case_stage_id,
         cs.previous_case_stage_id
  from   CASE_RECOMMENDATION cr, 
         case_stage cs
  where  cs.previous_case_stage_id = cr.case_stage_id (+)
  and    cs.stage_id = 1646;

alter table mv add constraint mv_ck check (
    (previous_case_stage_id is not null and case_stage_id is not null)
);
Chris Saxon
quelle
Fabelhaft, ich bin mit einem anderen MV darauf
gestoßen, habe
Ich mag diese Lösung nicht. Auf diese Weise eine Art referenzielle Integrität auszudrücken, ist möglich, aber sehr verwirrend. Sie haben darauf hingewiesen, dass die Überprüfung der Einschränkungen am Ende der Transaktion erfolgt. Das kann auch verwirrend sein. Auch die Implementierung der Prüfung durch Speichern dieser verknüpften Daten nimmt viel Platz in Anspruch. Sie implementieren die Tatsache, dass es n Empfehlungen für eine case_stage gibt, indem Sie n Zeilen speichern. Für mich sieht es so aus, als ob man die Summe der Zahlen a und b erhalten möchte. Man erhöht ein b-mal, um das Ergebnis zu erhalten.
Wunder173
Könnten Sie nur die Zeilen speichern, die im MV überprüft werden müssen (indem Sie der MV-Abfrage eine WHERE-Klausel hinzufügen)? Oder noch besser, speichern Sie nur die fehlerhaften Zeilen (damit das MV leer bleibt).
Vincent Malgrat
Guter Punkt Vincent, ich habe ein Update dafür hinzugefügt. Wenn mir ein Weg einfällt, alle Zeilen auszuschließen (oder jemand anderes kann es mir sagen!), Aktualisiere ich es erneut.
Chris Saxon
@ miracle173 - Ich habe nie gesagt, dass dies eine gute Lösung ist, nur dass es möglich ist! Ich stimme zu, dass dies kompliziert und möglicherweise verwirrend ist. Der zur Implementierung ähnlicher Funktionen in einer Mehrbenutzerumgebung erforderliche Prozedurcode ist jedoch wahrscheinlich auch kompliziert - insbesondere, wenn Löschvorgänge zulässig sind. Zumindest garantiert diese Lösung, dass sich die Datenbank nicht in einem inkonsistenten Zustand befindet, wie in den Regeln in der Frage definiert.
Chris Saxon
6

Wenn dies CASE_RECOMMENDATION.CASE_STAGE_IDeindeutig ist, können Sie die referenzielle Integrität mit einer virtuellen Spalte (11g +) verwenden, um sie bedingt zu machen:

alter table CASE_RECOMMENDATION ADD CONSTRAINT unique_case_stage unique (CASE_STAGE_ID);

-- virtual column only defined when stage_id=1646
alter table CASE_STAGE add 
   (case_1646 as (case when stage_id=1646 then previous_case_stage_id end));

-- check that the virtual column is defined when stage_id=1646
alter table case_stage add 
    constraint chk_1646 check ( stage_id!=1646 or previous_case_stage_id is not null);

-- referential integrity
alter table case_stage add 
     constraint fk_1646 foreign key (case_1646) 
     references case_recommendation (case_stage_id);

Lass uns nachsehen:

SQL> insert into  CASE_STAGE values (1, 1, 1, sysdate, null, null, 1, null, default);

1 row(s) inserted.

SQL> insert into CASE_RECOMMENDATION values (1, 1, 1, sysdate, 1);

1 row(s) inserted.

SQL> -- fails because previous_case_stage_id is null
SQL> insert into CASE_STAGE values (2, 1646, 1, sysdate, null, null, 1, null, default);

ORA-02290: check constraint (VNZ_TEST3.CHK_1646) violated

SQL> -- fails because previous_case_stage_id doesn't exist in CASE_RECOMMENDATION
SQL> insert into CASE_STAGE values (2, 1646, 1, sysdate, null, 2, 1, null, default); 

ORA-02291: integrity constraint (VNZ_TEST3.FK_1646) violated - parent key not found

SQL> -- succeeds !
SQL> insert into CASE_STAGE values (2, 1646, 1, sysdate, null, 1, 1, null, default); 

1 row(s) inserted.
Vincent Malgrat
quelle
Vielen Dank, eine großartige Antwort, die ich für andere "Regeln" verwenden kann, nur nicht für diese.
Kevinsky
3

Es ist bewundernswert, Geschäftslogik in die Datenbank aufzunehmen, und Sie sollten auf jeden Fall Einschränkungen für solche Dinge implementieren, wenn Sie können. Ein Trigger ist jedoch nicht die einzige Alternative. Sie können das Problem im PL / SQL-Paket lösen, das die Einfügung ausführt. Auf diese Weise erhalten Sie weitere Vorteile wie reduzierten clientseitigen Code, weniger Kontextwechsel, automatische Bindungen, Unabhängigkeit von Clientanwendungen usw. Hier ist ein unvollständiges Beispiel (ohne die Sperre, die erforderlich wäre, um dies gleichzeitig auszuführen).

CREATE OR REPLACE PACKAGE CaseStage As

Procedure InsertCaseStage (
   pId                      In Case_Stage.Id%Type,
   pStage_Id                In Case_Stage.Stage_Id%Type,
   pDate_Created            In Case_Stage.Date_Created%Type DEFAULT Current_Timestamp,
   pEnd_Reason_Id           In Case_Stage.End_Reason_Id%Type,
   pPrevious_Case_Stage_Id  In Case_Stage.Previous_Case_Stage_Id%Type,
   pCurrent                 In Case_Stage."CURRENT"%Type,
   pDate_Closed             In Case_Stage.Date_Closed%Type DEFAULT NULL
   );
END;
/

CREATE OR REPLACE PACKAGE BODY CaseStage As

Procedure InsertCaseStage (
   pId                      In Case_Stage.Id%Type,
   pStage_Id                In Case_Stage.Stage_Id%Type,
   pDate_Created            In Case_Stage.Date_Created%Type DEFAULT Current_Timestamp,
   pEnd_Reason_Id           In Case_Stage.End_Reason_Id%Type,
   pPrevious_Case_Stage_Id  In Case_Stage.Previous_Case_Stage_Id%Type,
   pCurrent                 In Case_Stage."CURRENT"%Type,
   pDate_Closed             In Case_Stage.Date_Closed%Type DEFAULT NULL
   ) 
As
   cPreviousCaseStageCheck Case_Stage.Stage_Id%Type := 1646;   
   vPreviousCount Number(1);
Begin
   If (pStage_Id = cPreviousCaseStageCheck) Then
      SELECT count(*) INTO vPreviousCount FROM Case_Recommendation 
      WHERE Case_Stage_Id = pPrevious_Case_Stage_Id
      AND rownum<=1;
      If (vPreviousCount <> 1) Then
         Raise_Application_Error(-20001,'Previous_Stage_Id must be found in '
            || 'Case_Recommendation. ' || pPrevious_Case_Stage_Id || ' was not.');
      End If;
   End If;

   /* INSERT... */
End;

END;
/
Leigh Riffel
quelle
Ich glaube sehr an Pakete und habe bereits eines, das CRUD-Operationen ausführt und diese Tabellen protokolliert. Ich kann die Verwendung des Pakets jedoch nicht erzwingen, wenn eine andere Datenbank versucht, Einfügungen über einen Link vorzunehmen. Ich muss mich auf die Sorgfalt des Programmierers verlassen. Eine Einschränkung ist sauberer, leichter zu erkennen und zu dokumentieren, aber Ihre Antwort würde funktionieren, wenn ein Paket verwendet würde.
Kevinsky
1
Während eine Prozedur definitiv die bessere Wahl ist, wenn Einschränkungen nicht verfügbar oder übermäßig kompliziert sind, ist es schwieriger, sie richtig zu codieren. Zum Beispiel kann Ihre Prozedur in einer Mehrbenutzerumgebung missbraucht werden: Benutzer A fügt einen 1646 ein, Benutzer B löscht die Empfehlung, Benutzer A legt fest => DB bleibt in einem inkonsistenten Zustand. Sie benötigen starke Sperren, um die gleichen Funktionen wie eine Einschränkung zu erreichen.
Vincent Malgrat
Hervorragender Punkt, jetzt verstehe ich, warum die Anwendung das Löschen von Einträgen in CASE_STAGE oder CASE_RECOMMENDATION nach dem Schließen der Phase nicht zulässt.
Kevinsky
1
@kevinsky - Ich stimme zu, dass diese Logik in einer Einschränkung besser wäre, unabhängig davon, warum Sie die Verwendung des Pakets nicht erzwingen können. Indem Sie niemanden als Tabellenbesitzer anmelden lassen und keine Einfügeberechtigungen für die Tabelle gewähren, können Sie sicherstellen, dass der gesamte Zugriff über das Paket erfolgt.
Leigh Riffel
@ Vincent Malgrat - Sie sind absolut richtig. Einschränkungen werden nach Möglichkeit bevorzugt und Sperren wären in einer gleichzeitigen Umgebung erforderlich.
Leigh Riffel
0

Ich vermisse eine Tabelle wie CASE_RECOMMENDATION_LIST (CASE_STAGE_ID NUMBER (9) PRIMARY KEY) in Ihrem Design. Jede CASE_RECOMMENDATION ist Mitglied der entsprechenden CASE_RECOMMENDATION_LIST. Dies kann von einem Fremdschlüssel behandelt werden. Eine CASE_RECOMMENDATION_LIST muss mindestens eine CASE_RECOMMENDATION enthalten. Das Erstellen und Löschen einer CASE_RECOMMENDATION_LIST kann durch einfache Trigger erfolgen: Erstellen Sie eine CASE_RECOMMENDATION_LIST für eine CASE_STAGE_ID, bevor die erste CASE_RECOMMENDATION für diese CASE_STAGE_ID erstellt wird. Löschen Sie sie, nachdem die letzte CASE_RECOMMENDATION für diese CASE_STGE_STGE_STGE_ID gelöscht wurde. CASE_STAGE referenziert höchstens eine CASE_RECOMMENDATION_LIST mit der CASE_STAGE.PREVIOUS_CASE_STAGE_ID. CASE_STAGE.PREVOUS_STAGE_ID darf nicht null sein, wenn CASE_STAGE.STAGE_ID = 1646.

Vielleicht wäre es eine bessere Möglichkeit, eine eigene Entität (und damit eine Tabelle) für die 1646 CASE_STAGEs zu erstellen, aber ich werde dies nicht weiter analysieren.

Wunder173
quelle