problem naruszenia ograniczenia klucza obcego

10

Zidentyfikowałem 3 sytuacje.

  1. Student bez zapisów.
  2. Student z zapisami, ale bez ocen.
  3. Student z zapisami i ocenami.

W tabeli rejestracji znajduje się wyzwalacz do obliczania GPA. Jeśli uczeń ma oceny, zaktualizuje lub wstawi wpis do tabeli GPA; bez ocen, bez wpisu tabeli GPA.

Mogę usunąć ucznia bez zapisów (nr 1). Mogę usunąć ucznia z zapisami i ocenami (punkt 3 powyżej). Ale nie mogę usunąć ucznia z zapisami, ale bez ocen (# 2). Dostaję naruszenie ograniczenia odniesienia.

Instrukcja DELETE była w konflikcie z ograniczeniem REFERENCE „FK_dbo.GPA_dbo.Student_StudentID”. Konflikt wystąpił w bazie danych „”, tabeli „dbo.GPA”, kolumnie „StudentID”.

Gdybym nie mógł usunąć nowego ucznia bez zapisów (i bez wpisu GPA), zrozumiałbym naruszenie ograniczenia, ale mogę usunąć tego ucznia. To student z zapisami i bez ocen (i wciąż bez wpisu GPA), których nie mogę usunąć.

Poprawiłem swój spust, aby móc iść do przodu. Teraz, jeśli masz rejestracje, wyzwalacz wstawia cię do tabeli GPA bez względu na wszystko. Ale nie rozumiem podstawowego problemu. Wszelkie wyjaśnienia będą mile widziane.

Tyle ile jest warte:

  1. Visual Studio 2013 Professional.
  2. IIS express (wewnętrzny do VS2013).
  3. Aplikacja internetowa ASP.NET przy użyciu EntityFramework 6.1.1.
  4. MS SQL Server 2014 Enterprise.
  5. GPA.Value ma wartość null.
  6. Enrollment.GradeID ma wartość null.

Oto fragment bazy danych:

obraz bazy danych

- EDYCJA -

Wszystkie tabele są tworzone przez EntityFramework, użyłem SQL Server Management Studio do ich wytworzenia.

Oto instrukcje tworzenia tabeli z ograniczeniami .:

GPA stół:

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 stół:

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 stół:

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]

Oto czynniki uruchamiające :

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

Łatka do przodu polegała na komentowaniu tych linii w AFTER INSERTwyzwalaczu.

Oto procedura składowana :

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

Oto funkcja bazy danych :

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

Oto dane wyjściowe debugowania z metody usuwania kontrolera, instrukcja select jest metodą sprawdzającą, co należy usunąć. Ten uczeń ma 3 zapisy, REFERENCEproblem z ograniczeniem pojawia się po usunięciu 3. zapisu. Zakładam, że EF używa transakcji, ponieważ rejestracje nie są usuwane.

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
źródło

Odpowiedzi:

7

To kwestia czasu. Rozważ usunięcie StudentID nr 1:

  1. Wiersz jest usuwany z Studenttabeli
  2. Kaskadowe usuwanie usuwa odpowiednie wiersze z Enrollment
  3. Relacja klucza obcego GPA-> Studentjest sprawdzana
  4. Spust uruchamia się, wzywając MergeGPA

W tym momencie MergeGPAsprawdza , czy w GPAtabeli znajduje się wpis dla ucznia nr 1 . Nie ma (w przeciwnym razie sprawdzenie FK w kroku 3 spowodowałoby błąd).

Tak więc WHEN NOT MATCHEDklauzula przy MergeGPApróbie INSERTwprowadzenia wiersza GPAdla StudentID # 1. Ta próba kończy się niepowodzeniem (z błędem FK), ponieważ StudentID # 1 został już usunięty z Studenttabeli (w kroku 1).

Paul White 9
źródło
1
Myślę, że coś masz na myśli. Kiedy uczeń jest tworzony z zapisami, ale nie przypisano żadnych ocen, ten uczeń nie ma wpisu w tabeli GPA. Gdy baza danych idzie do usunięcia tego ucznia, przegląda bazę danych, widzi zapisy do usunięcia, ale nie ma wpisu GPA. Więc chodzi o usunięcie rejestracji, które powodują uruchomienie wyzwalacza, który tworzy pozycję GPA, a następnie powoduje naruszenie ograniczenia? Tak więc rozwiązaniem jest utworzenie wpisu GPA podczas tworzenia studenta. Wtedy mój wyzwalacz wstawiania nie będzie wymagał warunkowego, a moja procedura składowana nie będzie musiała być scaleniem, a jedynie aktualizacją.
DowntownHippie
-1

Bez czytania wszystkich, tylko ze schematu: Masz wpis w zapisie lub wpis w GPA wskazujący na Studenta, którego chcesz usunąć.

Wpisy z kluczami obcymi muszą zostać najpierw usunięte (lub klucze ustawione na null, ale jest to zła praktyka), zanim będzie można usunąć wpis Studenta.

Również niektóre bazy danych mają opcję USUŃ KASKADĘ, która usunie wszystkie wpisy z kluczami obcymi do tego, który chcesz usunąć.

Innym sposobem jest nie deklarowanie ich jako kluczy obcych i używanie tylko wartości klucza, ale nie jest to również zalecane.

użytkownik44286
źródło
W przypadkach, w których się nie udaje, jest wpis w rejestracji, ale nie ma go w GPA.
DowntownHippie
masz pewne ograniczenia związane z ON DELETE CASCADE, a niektóre bez. spróbuj dodać tę linię do wszystkich ograniczeń. po tym spróbuje wyłączyć wszystkie wyzwalacze, a po tym teście przy minimalnej konfiguracji. powodzenia
użytkownik44286,
Widzę te ON DELETE CASCADEoświadczenia. Żadna z tych instrukcji tworzenia tabeli ani instrukcje usuwania nie są pisane ręcznie, wszystkie są generowane przez bytowanie. Kaskady są spowodowane tym, że rejestracja ma klucze obce, które nie są kluczem podstawowym; Ograniczeniem klucza obcego GPA jest to klucz podstawowy, więc nie powinna wymagać kaskady. Przetestowałem to, jeśli usuniesz ucznia z wpisem w tabeli GPA, wpis zostanie usunięty. Jedynym problemem jest student z zapisami, ale bez GPA.
DowntownHippie