problema de violação de restrição de chave estrangeira

10

Eu identifiquei 3 situações.

  1. Um aluno sem matrículas.
  2. Um aluno com matrículas, mas sem notas.
  3. Um aluno com matrículas e notas.

Há um gatilho na tabela de inscrições para calcular o GPA. Se um aluno tiver notas, ele atualizará ou inserirá uma entrada na tabela GPA; sem notas, sem entrada na tabela GPA.

Posso excluir um aluno sem matrículas (nº 1). Posso excluir um aluno com matrículas e notas (nº 3 acima). Mas não posso excluir um aluno com inscrições, mas sem notas (nº 2). Eu recebo uma violação de restrição de referência.

A instrução DELETE entra em conflito com a restrição REFERENCE "FK_dbo.GPA_dbo.Student_StudentID". O conflito ocorreu no banco de dados "", tabela "dbo.GPA", coluna 'StudentID'.

Se eu não conseguisse excluir um novo aluno sem inscrições (e nenhuma entrada no GPA), entenderia a violação de restrição, mas posso excluir esse aluno. É um aluno com matrículas e sem notas (e ainda sem inscrição no GPA) que não posso excluir.

Eu apertei meu gatilho para poder seguir em frente. Agora, se você tiver inscrições, o gatilho o inserirá na tabela GPA, não importa o quê. Mas não entendo o problema subjacente. Qualquer explicação seria muito apreciada.

Pelo que vale a pena:

  1. Visual Studio 2013 Professional.
  2. IIS express (interno ao VS2013).
  3. ASP.NET Web App usando EntityFramework 6.1.1.
  4. MS SQL Server 2014 Enterprise.
  5. GPA.Value é anulável.
  6. Enrollment.GradeID é anulável.

Aqui está um trecho do banco de dados:

imagem de banco de dados

- EDITAR -

As tabelas são todas criadas pelo EntityFramework, usei o SQL Server Management Studio para produzi-las.

Aqui estão as instruções de criação de tabela com restrições:

GPA tabela:

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 tabela:

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 tabela:

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]

Aqui estão os gatilhos :

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

O patch para avançar era comentar essas linhas no AFTER INSERTgatilho.

Aqui está o procedimento armazenado :

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

Aqui está a função de banco de dados :

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

Aqui está a saída de depuração do método delete do controlador, a instrução select é o método que consulta o que excluir. Este aluno tem três matrículas, o REFERENCEproblema de restrição acontece quando a terceira matrícula é excluída. Presumo que a EF esteja usando uma transação porque as inscrições não foram excluídas.

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.
Centro da cidade
fonte

Respostas:

7

É uma questão de tempo. Considere excluir StudentID # 1:

  1. A linha é excluída da Studenttabela
  2. A exclusão em cascata remove as linhas correspondentes de Enrollment
  3. O relacionamento de chave estrangeira GPA-> Studentestá marcado
  4. O gatilho dispara, chamando MergeGPA

Nesse ponto, MergeGPAverifica se há uma entrada para o aluno nº 1 na GPAtabela. Não existe (caso contrário, a verificação do FK na etapa 3 teria gerado um erro).

Portanto, a WHEN NOT MATCHEDcláusula MergeGPAtenta entrar INSERTem linha GPApara StudentID # 1. Esta tentativa falha (com o erro FK) porque o StudentID # 1 já foi excluído da Studenttabela (na etapa 1).

Paul White 9
fonte
11
Eu acho que você está em algo. Quando um aluno é criado com matrículas, mas nenhuma nota foi atribuída, ele não tem entrada na tabela GPA. Quando o banco de dados exclui esse aluno, ele olha para o banco de dados e vê inscrições para excluir, mas nenhuma entrada no GPA. Por isso, ele decide excluir as inscrições, o que causa o disparo de um gatilho que cria a entrada GPA, que causa a violação de restrição? Portanto, a solução é criar uma entrada no GPA quando eu criar um aluno. Então meu gatilho de inserção não precisará de uma condicional e meu procedimento armazenado não precisará ser uma mesclagem, apenas uma atualização.
DowntownHippie
-1

Sem ler tudo, apenas no diagrama: você tem uma entrada na inscrição ou uma no GPA apontando para o aluno que deseja excluir.

As entradas com as chaves estrangeiras precisam ser excluídas primeiro (ou as chaves definidas como nulas, mas isso é uma prática recomendada) antes que você possa excluir a entrada do Aluno.

Além disso, alguns bancos de dados têm ON DELETE CASCADE, que excluirá quaisquer entradas com chaves estrangeiras para a que você deseja excluir.

Outra maneira é não declará-las como chaves estrangeiras e usar apenas o valor da chave, mas isso também não é recomendado.

user44286
fonte
Nos casos em que está falhando, há uma entrada em Inscrição, mas não uma no GPA.
DowntownHippie
você tem algumas restrições com ON DELETE CASCADE e outras sem. tente adicionar essa linha a todas as restrições. depois disso, tentaria desativar todos os gatilhos e depois desse teste com uma configuração mínima. Boa sorte!
user44286
Eu vejo essas ON DELETE CASCADEdeclarações. Nenhuma dessas instruções de criação de tabela, nem as instruções de exclusão são escritas à mão, todas são geradas pelo framework de entidade. As cascatas são porque o registro tem chaves estrangeiras que não são a chave primária; A restrição de chave estrangeira do GPA é sua chave primária, portanto não deve precisar de uma cascata. Eu testei isso, se você excluir um aluno com uma entrada da tabela GPA, a entrada será excluída. O único problema é um aluno com inscrições, mas sem gpa.
DowntownHippie