ТРИГГЕР ПОСЛЕ ОБНОВЛЕНИЯ, чтобы поддерживать актуальность общего количества кредитов

В моей базе данных University есть таблицы Student, Takes и Course:

  • Student (идентификатор, имя, название отдела, общее количество кредитов)
  • Takes (StudentId, CourseId,sectionId, семестр, год, оценка)
  • Course (Идентификатор курса, Должность, Название отдела, Кредиты)

Я хочу написать триггер для автоматического обновления TotalCredits студента, который получает приемлемую оценку (когда преподаватель обновляет оценку за курс, пройденный студентом).

Я написал этот триггер и выполнил его:

CREATE TRIGGER [dbo].[credits_earned]  
ON [dbo].[Takes] 
AFTER UPDATE
AS
BEGIN
    IF UPDATE(grade)
    BEGIN
        DECLARE @new_grade varchar(2)

        SELECT @new_grade = I.grade
        FROM inserted I 

        DECLARE @old_grade varchar(2)

        SELECT @old_grade = T.grade
        FROM Takes T

        IF @new_grade<>'F' AND @new_grade IS NOT NULL
             AND (@old_grade = 'F' OR @old_grade IS NULL)
        BEGIN
            UPDATE Student
            SET tot_cred = tot_cred + 
                        (SELECT credits
                         FROM Course C
                         WHERE C.course_id = (SELECT I.course_id 
                                              FROM inserted I))
            WHERE Student.id = (SELECT I.id FROM inserted I)
        END
    END
END

И команды завершились успешно.

Но когда я выполняю процедуру обновления оценки курса студента (первая оценка в таблице дублей равна нулю), триггер не влияет на TotalCredits этого студента.

Буду признателен за любой ответ, который вы напишете.

Ваш триггер фатально неисправен; предполагается, что UPDATE может влиять только на одну строку; Это просто не соответствует действительности. От вредных привычек: злоупотребление триггерами и «Тихая ошибка», которую я нахожу в большинстве триггеров

Thom A 09.04.2024 10:30

Вероятно, вашим триггером должен быть просто просмотр.

siggemannen 09.04.2024 11:24
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
80
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Этот тип триггера — очень плохая идея, и его следует избегать, если это вообще возможно, поскольку он может привести к проблемам. Если логика обнаружения изменений когда-либо ошибается, вы получаете неверные данные. Если в вашем триггере возникнет ошибка, вы получите рассинхронизированные данные. В идеале вы всегда должны вычислять эту информацию, когда она вам нужна, например. с представлением, а не хранить вычисленное значение.


Однако, если вам нужно использовать триггер, при написании логики триггера это все равно SQL, и, следовательно, он все равно должен быть основан на наборах, а не на процедурах (поскольку SQL Server оптимизирован для операций на основе наборов).

Теперь я думаю, что следующее работает. (Я делаю предположение о том, что уникально идентифицирует Take, чтобы соединить Inserted с Deleted (именно так вы обнаруживаете изменения).

И тогда нам нужно предварительно агрегировать, потому что ваша агрегация добавила бы баллы для всех Takes независимо от того, изменилась ли оценка.

Также обратите внимание, что я склонен инвертировать флажок UPDATE, чтобы было ясно, что это вариант досрочного выхода.

    IF NOT UPDATE(Grade) RETURN;

    UPDATE s SET
        tot_cred = tot_cred + c.Credits
    FROM Student s
    JOIN (
        SELECT i.StudentId, SUM(c.Credits) Credits
        FROM Inserted i
        JOIN Deleted d ON d.StudentId = i.StudentId
            AND d.CourseId = i.CourseId
            AND d.SectionId = i.SectionId
            AND d.Semester = i.Semester
            AND d.Year = i.Year
        JOIN Course c ON c.CourseId = i.CourseId
        WHERE i.Grade <> 'F' AND i.Grade IS NOT NULL
        AND (d.Grade = 'F' OR d.Grade IS NULL)
        GROUP BY i.StudentId
    ) c ON c.Student = s.Id;

Более того, если ваш триггер обновляет инкрементное значение, это более рискованный подход, потому что, если итоговое значение когда-либо окажется неверным, оно так и останется неверным. Обычно лучше каждый раз рассчитывать общую сумму с нуля, например:

    UPDATE s SET
        tot_cred = (
            -- Whatever logic gives the accurate credits at a point in time
            SELECT SUM(c.Credits)
            FROM Takes t
            JOIN Courses c on c.CourseId = t.CourseId
            WHERE t.StudentId = s.Id
            AND t.Grade <> 'F'
        )
    FROM Student s
    WHERE s.id IN (
        SELECT i.StudentId
        FROM Inserted i
        JOIN Deleted d ON d.StudentId = i.StudentId
            AND d.CourseId = i.CourseId
            AND d.SectionId = i.SectionId
            AND d.Semester = i.Semester
            AND d.Year = i.Year
        WHERE i.Grade <> 'F' AND i.Grade IS NOT NULL
        AND (d.Grade = 'F' OR d.Grade IS NULL)
    )

Это, вероятно, можно еще упростить следующим образом - что может обновить случайного студента, где ничего не изменилось - но это небольшая цена за простоту и скорость:

    UPDATE s SET
        tot_cred = (
            -- Whatever logic gives the accurate credits at a point in time
            SELECT SUM(c.Credits)
            FROM Takes t
            JOIN Courses c on c.CourseId = t.CourseId
            WHERE t.StudentId = s.Id
            AND t.Grade <> 'F'
        )
    FROM Student s
    WHERE s.id IN (
        SELECT i.StudentId
        FROM Inserted i
        WHERE i.Grade <> 'F'
    )

ИМХО, ваша заметка должна быть реальным ответом... Я имею в виду что-то вроде: «Вам действительно следует стараться избегать хранения данных, которые можно легко вычислить, потому что ...., но если вам действительно нужно, вот как вам следует написать свой триггер" - но эй, это действительно зависит от тебя. В любом случае, вы получили мой голос.

Zohar Peled 09.04.2024 11:40

Слышу вас, нам всем когда-то приходилось идти по пути плохих решений :-)

Zohar Peled 09.04.2024 12:06

У вас есть фатальный недостаток в вашем триггере: вы не учитываете несколько (или ноль) строк в таблице inserted. Вам также необходимо учитывать вставки и удаления. А функция UPDATE не сообщает вам, действительно ли столбец изменился, только если он присутствовал в операторе.

Более того, если вы запросите реальную таблицу, вы получите новые результаты. Вам нужно воспользоваться deleted таблицей.

Следующий триггер будет учитывать несколько строк, а также вставки, обновления и удаления. Мы объединяем inserted и deleted, беря 1 и -1 соответственно, а затем прибавляя кредиты.

CREATE OR ALTER TRIGGER [dbo].[credits_earned]  
ON [dbo].[Takes] 
AFTER INSERT, UPDATE, DELETE
AS

SET NOCOUNT ON;

IF NOT EXISTS (SELECT 1 FROM inserted) AND NOT EXISTS (SELECT 1 FROM deleted)
    RETURN;

UPDATE Student
SET tot_cred += i.diff
FROM Student s
JOIN (
    SELECT
      i.student_id,
      SUM(i.diff * c.credits) AS diff
    FROM (
        SELECT i.student_id, i.course_id, IIF(i.grade = 'F', 1, 0) AS diff
        FROM inserted 

        UNION ALL

        SELECT d.student_id, d.course_id, IIF(i.grade = 'F', -1, 0)
        FROM deleted d
    ) i
    JOIN Course c ON c.course_id = i.course_id
    GROUP BY
      i.student_id
) i ON i.student_id = s.id
WHERE i.diff <> 0;

Сказав все это, я все равно не рекомендовал бы это. Это можно реализовать гораздо эффективнее, используя Индексированное представление.

CREATE OR ALTER VIEW dbo.Student_TotalCredit
WITH SCHEMABINDING
AS

SELECT
  s.id,
  COUNT_BIG(*) AS count,  -- must have a count
  SUM(ISNULL(IIF(t.grade = 'F', c.credits, 0), 0)) AS total_credit
FROM dbo.Student s
JOIN dbo.Takes t ON t.student_id = s.id
JOIN Course c ON c.course_id = t.course_id
GROUP BY
  s.id;
CREATE UNIQUE CLUSTERED INDEX IX ON dbo.Student_TotalCredit (id);

Хорошая работа, как всегда :)

Dale K 09.04.2024 21:57

Другие вопросы по теме