Problem mit Verstößen gegen Fremdschlüsseleinschränkungen

10

Ich habe 3 Situationen identifiziert.

  1. Ein Student ohne Einschreibungen.
  2. Ein Student mit Einschreibungen, aber ohne Noten.
  3. Ein Student mit Einschreibungen und Noten.

In der Registrierungstabelle befindet sich ein Auslöser für die Berechnung des GPA. Wenn ein Schüler Noten hat, wird er aktualisiert oder einen Eintrag in die GPA-Tabelle einfügen. Keine Noten, kein GPA-Tabelleneintrag.

Ich kann einen Studenten ohne Einschreibung löschen (# 1). Ich kann einen Schüler mit Einschreibungen und Noten löschen (Nr. 3 oben). Aber ich kann keinen Studenten mit Einschreibungen, aber ohne Noten löschen (# 2). Ich erhalte eine Verletzung der Referenzbedingung.

Die DELETE-Anweisung stand in Konflikt mit der REFERENCE-Einschränkung "FK_dbo.GPA_dbo.Student_StudentID". Der Konflikt trat in der Datenbank "", Tabelle "dbo.GPA", Spalte 'StudentID' auf.

Wenn ich einen neuen Schüler ohne Einschreibungen (und ohne GPA-Eintrag) nicht löschen könnte, würde ich die Verletzung der Einschränkung verstehen, aber ich kann diesen Schüler löschen. Es ist ein Student mit Einschreibungen und ohne Noten (und immer noch ohne GPA-Eintrag), den ich nicht löschen kann.

Ich habe meinen Abzug gepatcht, damit ich vorwärts gehen kann. Wenn Sie jetzt Registrierungen haben, fügt der Trigger Sie in die GPA-Tabelle ein, egal was passiert. Aber ich verstehe das zugrunde liegende Problem nicht. Jede Erklärung wäre sehr dankbar.

Für das, was es wert ist:

  1. Visual Studio 2013 Professional.
  2. IIS Express (intern für VS2013).
  3. ASP.NET Web App mit EntityFramework 6.1.1.
  4. MS SQL Server 2014 Enterprise.
  5. GPA.Value ist nullbar.
  6. Enrollment.GradeID ist nullwertfähig.

Hier ist ein Ausschnitt aus der Datenbank:

Datenbankbild

- BEARBEITEN -

Die Tabellen werden alle von EntityFramework erstellt. Ich habe SQL Server Management Studio verwendet, um diese zu erstellen.

Hier sind die Anweisungen zum Erstellen von Tabellen mit Einschränkungen:

GPA Tabelle:

CREATE TABLE [dbo].[GPA](
    [StudentID] [int] NOT NULL,
    [Value] [float] NULL,
  CONSTRAINT [PK_dbo.GPA] PRIMARY KEY CLUSTERED 
  (
    [StudentID] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
         ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[GPA]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.GPA_dbo.Student_StudentID] 
  FOREIGN KEY([StudentID])
  REFERENCES [dbo].[Student] ([ID])

ALTER TABLE [dbo].[GPA] 
  CHECK CONSTRAINT [FK_dbo.GPA_dbo.Student_StudentID]

Enrollment Tabelle:

CREATE TABLE [dbo].[Enrollment](
    [EnrollmentID] [int] IDENTITY(1,1) NOT NULL,
    [CourseID] [int] NOT NULL,
    [StudentID] [int] NOT NULL,
    [GradeID] [int] NULL,
  CONSTRAINT [PK_dbo.Enrollment] PRIMARY KEY CLUSTERED 
  (
    [EnrollmentID] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
         ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Enrollment]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.Enrollment_dbo.Course_CourseID] 
  FOREIGN KEY([CourseID])
  REFERENCES [dbo].[Course] ([CourseID])
  ON DELETE CASCADE

ALTER TABLE [dbo].[Enrollment] 
  CHECK CONSTRAINT [FK_dbo.Enrollment_dbo.Course_CourseID]

ALTER TABLE [dbo].[Enrollment]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.Enrollment_dbo.Grade_GradeID] 
  FOREIGN KEY([GradeID])
  REFERENCES [dbo].[Grade] ([GradeID])

ALTER TABLE [dbo].[Enrollment] 
  CHECK CONSTRAINT [FK_dbo.Enrollment_dbo.Grade_GradeID]

ALTER TABLE [dbo].[Enrollment]  WITH CHECK 
  ADD  CONSTRAINT [FK_dbo.Enrollment_dbo.Student_StudentID] 
  FOREIGN KEY([StudentID])
  REFERENCES [dbo].[Student] ([ID])
  ON DELETE CASCADE

ALTER TABLE [dbo].[Enrollment] 
  CHECK CONSTRAINT [FK_dbo.Enrollment_dbo.Student_StudentID]

Student Tabelle:

CREATE TABLE [dbo].[Student](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [EnrollmentDate] [datetime] NOT NULL,
    [LastName] [nvarchar](50) NOT NULL,
    [FirstName] [nvarchar](50) NOT NULL,
  CONSTRAINT [PK_dbo.Student] PRIMARY KEY CLUSTERED 
  (
    [ID] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, 
         ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Hier sind die Auslöser :

CREATE TRIGGER UpdateGPAFromUpdateDelete
ON Enrollment
AFTER UPDATE, DELETE AS
BEGIN
    DECLARE @UpdatedStudentID AS int
    SELECT @UpdatedStudentID = StudentID FROM DELETED
    EXEC MergeGPA @UpdatedStudentID
END

CREATE TRIGGER UpdateGPAFromInsert
ON Enrollment
AFTER INSERT AS
--DECLARE @InsertedGradeID AS int
--SELECT @InsertedGradeID = GradeID FROM INSERTED
--IF @InsertedGradeID IS NOT NULL
    BEGIN
        DECLARE @InsertedStudentID AS int
        SELECT @InsertedStudentID = StudentID FROM INSERTED
        EXEC MergeGPA @InsertedStudentID
    END

Der Patch, um vorwärts zu kommen, bestand darin, diese Zeilen im AFTER INSERTTrigger auskommentieren.

Hier ist die gespeicherte Prozedur :

CREATE PROCEDURE MergeGPA @StudentID int AS
MERGE GPA AS TARGET
USING (SELECT @StudentID) as SOURCE (StudentID)
ON (TARGET.StudentID = SOURCE.StudentID)
WHEN MATCHED THEN
    UPDATE
        SET Value = (SELECT Value FROM GetGPA(@StudentID))
WHEN NOT MATCHED THEN
INSERT (StudentID, Value)
    VALUES(SOURCE.StudentID, (SELECT Value FROM GetGPA(@StudentID)));

Hier ist die Datenbank - Funktion :

CREATE FUNCTION GetGPA (@StudentID int) 
RETURNS TABLE
AS RETURN
SELECT ROUND(SUM (StudentTotal.TotalCredits) / SUM (StudentTotal.Credits), 2) Value
    FROM (
        SELECT 
            CAST(Credits as float) Credits
            , CAST(SUM(Value * Credits) as float) TotalCredits
        FROM 
            Enrollment e 
            JOIN Course c ON c.CourseID = e.CourseID
            JOIN Grade g  ON e.GradeID = g.GradeID
        WHERE
            e.StudentID = @StudentID AND
            e.GradeID IS NOT NULL
        GROUP BY
            StudentID
            , Value
            , e.courseID
            , Credits
    ) StudentTotal

Hier ist die Debug-Ausgabe der Löschmethode des Controllers. Die select-Anweisung ist die Methode, die abfragt, was gelöscht werden soll. Dieser Schüler hat 3 Einschreibungen. Das REFERENCEProblem mit den Einschränkungen tritt auf, wenn die 3. Einschreibung gelöscht wird. Ich gehe davon aus, dass EF eine Transaktion verwendet, da die Registrierungen nicht gelöscht werden.

iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.ReaderExecuted;Timespan:00:00:00.0004945;Properties:
Command: SELECT 
    [Project2].[StudentID] AS [StudentID], 
    [Project2].[ID] AS [ID], 
    [Project2].[EnrollmentDate] AS [EnrollmentDate], 
    [Project2].[LastName] AS [LastName], 
    [Project2].[FirstName] AS [FirstName], 
    [Project2].[Value] AS [Value], 
    [Project2].[C1] AS [C1], 
    [Project2].[EnrollmentID] AS [EnrollmentID], 
    [Project2].[CourseID] AS [CourseID], 
    [Project2].[StudentID1] AS [StudentID1], 
    [Project2].[GradeID] AS [GradeID]
    FROM ( SELECT 
        [Limit1].[ID] AS [ID], 
        [Limit1].[EnrollmentDate] AS [EnrollmentDate], 
        [Limit1].[LastName] AS [LastName], 
        [Limit1].[FirstName] AS [FirstName], 
        [Limit1].[StudentID] AS [StudentID], 
        [Limit1].[Value] AS [Value], 
        [Extent3].[EnrollmentID] AS [EnrollmentID], 
        [Extent3].[CourseID] AS [CourseID], 
        [Extent3].[StudentID] AS [StudentID1], 
        [Extent3].[GradeID] AS [GradeID], 
        CASE WHEN ([Extent3].[EnrollmentID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (2) 
            [Extent1].[ID] AS [ID], 
            [Extent1].[EnrollmentDate] AS [EnrollmentDate], 
            [Extent1].[LastName] AS [LastName], 
            [Extent1].[FirstName] AS [FirstName], 
            [Extent2].[StudentID] AS [StudentID], 
            [Extent2].[Value] AS [Value]
            FROM  [dbo].[Student] AS [Extent1]
            LEFT OUTER JOIN [dbo].[GPA] AS [Extent2] ON [Extent1].[ID] = [Extent2].[StudentID]
            WHERE [Extent1].[ID] = @p__linq__0 ) AS [Limit1]
        LEFT OUTER JOIN [dbo].[Enrollment] AS [Extent3] ON [Limit1].[ID] = [Extent3].[StudentID]
    )  AS [Project2]
    ORDER BY [Project2].[StudentID] ASC, [Project2].[ID] ASC, [Project2].[C1] ASC: 
iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.NonQueryExecuted;Timespan:00:00:00.0012696;Properties:
Command: DELETE [dbo].[Enrollment]
WHERE ([EnrollmentID] = @0): 
iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.NonQueryExecuted;Timespan:00:00:00.0002634;Properties:
Command: DELETE [dbo].[Enrollment]
WHERE ([EnrollmentID] = @0): 
iisexpress.exe Information: 0 : Component:SQL Database;Method:SchoolInterceptor.NonQueryExecuted;Timespan:00:00:00.0002512;Properties:
Command: DELETE [dbo].[Enrollment]
WHERE ([EnrollmentID] = @0): 
iisexpress.exe Error: 0 : Error executing command: DELETE [dbo].[Student]
WHERE ([ID] = @0) Exception: System.Data.SqlClient.SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.GPA_dbo.Student_StudentID". The conflict occurred in database "<databasename>", table "dbo.GPA", column 'StudentID'.
The statement has been terminated.
DowntownHippie
quelle

Antworten:

7

Es ist eine Frage des Timings. Löschen Sie möglicherweise die StudentID # 1:

  1. Die Zeile wird aus der StudentTabelle gelöscht
  2. Durch das Löschen der Kaskade werden entsprechende Zeilen aus entfernt Enrollment
  3. Die Fremdschlüsselbeziehung GPA-> Studentwird überprüft
  4. Der Auslöser wird ausgelöst und ruft MergeGPA

MergeGPAÜberprüfen Sie zu diesem Zeitpunkt, ob in der GPATabelle ein Eintrag für Schüler Nr. 1 vorhanden ist . Dies ist nicht der Fall (andernfalls hätte die FK-Prüfung in Schritt 3 einen Fehler ausgelöst).

Die WHEN NOT MATCHEDKlausel MergeGPAversucht also, INSERTeine Zeile GPAfür StudentID # 1 einzugeben. Dieser Versuch schlägt fehl (mit dem FK-Fehler), da StudentID # 1 bereits aus der StudentTabelle gelöscht wurde (in Schritt 1).

Paul White 9
quelle
1
Ich denke du bist auf etwas. Wenn ein Schüler mit Einschreibungen erstellt wird, aber keine Noten zugewiesen wurden, hat dieser Schüler keinen Eintrag in der GPA-Tabelle. Wenn die Datenbank diesen Schüler löscht, sieht sie sich die Datenbank an und sieht zu löschende Anmeldungen, aber keinen GPA-Eintrag. Es geht also darum, die Registrierungen zu löschen, wodurch ein Auslöser ausgelöst wird, der den GPA-Eintrag erstellt, der dann die Einschränkungsverletzung verursacht. Die Lösung besteht also darin, einen GPA-Eintrag zu erstellen, wenn ich einen Schüler erstelle. Dann benötigt mein Einfügetrigger keine Bedingung, und meine gespeicherte Prozedur muss keine Zusammenführung sein, sondern nur eine Aktualisierung.
DowntownHippie
-1

Ohne alles zu lesen, nur aus dem Diagramm: Sie haben entweder einen Eintrag in der Registrierung oder einen in GPA, der auf den Schüler verweist, den Sie löschen möchten.

Die Einträge mit den Fremdschlüsseln müssen zuerst gelöscht werden (oder die Schlüssel werden auf null gesetzt, aber das ist eine schlechte Praxis), bevor Sie den Schülereintrag löschen können.

Einige Datenbanken verfügen auch über ON DELETE CASCADE, wodurch alle Einträge mit Fremdschlüsseln zu dem Eintrag gelöscht werden, den Sie löschen möchten.

Eine andere Möglichkeit besteht darin, sie nicht als Fremdschlüssel zu deklarieren und nur den Schlüsselwert zu verwenden. Dies wird jedoch auch nicht empfohlen.

user44286
quelle
In den Fällen, in denen dies fehlschlägt, gibt es einen Eintrag in der Registrierung, jedoch keinen in GPA.
DowntownHippie
Sie haben einige Einschränkungen mit ON DELETE CASCADE und einige ohne. Versuchen Sie, diese Zeile allen Einschränkungen hinzuzufügen. Danach würden Sie versuchen, alle Trigger zu deaktivieren und danach mit einem minimalen Setup zu testen.
Viel
Ich sehe diese ON DELETE CASCADEAussagen. Weder diese Tabellenerstellungsanweisungen noch die Löschanweisungen sind handgeschrieben, sie werden alle vom entityframework generiert. Die Kaskaden sind darauf zurückzuführen, dass die Registrierung Fremdschlüssel enthält, die nicht der Primärschlüssel sind. Die Fremdschlüsseleinschränkung von GPA ist der Primärschlüssel, daher sollte keine Kaskade erforderlich sein. Ich habe dies getestet. Wenn Sie einen Schüler mit einem GPA-Tabelleneintrag löschen, wird der Eintrag gelöscht. Das einzige Problem ist ein Student mit Einschreibungen, aber ohne gpa.
DowntownHippie