ВЫБРАТЬ * нормально в триггере. Или я прошу о неприятностях?

8

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

Представьте себе сценарий, в котором триггер используется для копирования удаленных записей в таблицу аудита. Триггер использует SELECT *. Все указывают и кричат ​​и говорят нам, как это плохо.

Однако, если в структуру основной таблицы внесены изменения, а таблица аудита пропущена, то триггер выдаст ошибку, сообщающую людям, что таблица аудита также нуждается в модификации.

Ошибка будет обнаружена во время тестирования на наших серверах DEV. Но нам нужно обеспечить соответствие производственного процесса DEV, поэтому мы разрешаем SELECT * в производственных системах (только триггеры).

Итак, мой вопрос: меня подталкивают к удалению SELECT *, но я не уверен, как еще обеспечить автоматическую фиксацию ошибок разработки такого рода, каких-либо идей или это лучшая практика?

Я собрал пример ниже:

--Create Test Table
CREATE TABLE dbo.Test(ID INT IDENTITY(1,1), Person VARCHAR(255))
--Create Test Audit Table
CREATE TABLE dbo.TestAudit(AuditID INT IDENTITY(1,1),ID INT, Person VARCHAR(255))

--Create Trigger on Test
CREATE TRIGGER [dbo].[trTestDelete] ON [dbo].[Test] AFTER DELETE
NOT FOR REPLICATION
AS
BEGIN
    SET NOCOUNT ON;
    INSERT  dbo.TestAudit([ID], [Person])
    SELECT  *
    FROM    deleted
END

--Insert Test Data into Test
INSERT INTO dbo.Test VALUES
('Scooby')
,('Fred')
,('Shaggy')

--Perform a delete
DELETE dbo.Test WHERE Person = 'Scooby'

ОБНОВЛЕНИЕ (перефразировать вопрос):

Я администратор базы данных и должен убедиться, что разработчики не предоставляют плохо продуманные сценарии развертывания, внося вклад в нашу документацию по передовому опыту. SELECT * вызывает ошибку в DEV, когда разработчик пропускает таблицу аудита (это защитная сеть), поэтому ошибка обнаруживается на ранней стадии процесса разработки. Но где-то в Конституции SQL - вторая поправка гласит: «Не используйте SELECT *». Так что теперь есть толчок, чтобы избавиться от сети безопасности.

Как бы вы заменили сеть безопасности, или я должен считать это лучшей практикой для триггеров?

ОБНОВЛЕНИЕ 2: (решение)

Спасибо за ваш вклад, я не уверен, что у меня есть четкий ответ, потому что это, кажется, очень серый вопрос. Но вместе вы предоставили темы для обсуждения, которые могут помочь нашим разработчикам продвинуться вперед в определении своей лучшей практики.

Спасибо Daevinза ваш вклад, ваш ответ обеспечивает основу для некоторых механизмов тестирования, которые могут реализовать наши разработчики. +1

Спасибо CM_Dayton, ваши предложения, способствующие передовому опыту, могут быть полезны всем, кто разрабатывает триггеры аудита. +1

Большое спасибо ypercube, вы много размышляли о проблемах, связанных с изменениями в определениях таблиц. +1

В заключении:

Is Select * ok in a tigger? Да, это серая зона, не слепо следуйте идеологии «Выбрать * - это плохо».

Am I asking for Trouble? Да, мы делаем больше, чем просто добавляем новые столбцы в таблицы.

pacreely
источник
Вы отвечаете себе в вопросе. select * прервется, если исходная таблица будет изменена. Чтобы убедиться, что dev и prod одинаковы, используйте некоторую форму контроля версий.
Боб Климс
чуть более широкий вопрос, как часто вы удаляете записи и сколько составляет общий процент от таблицы? альтернативой триггерам будет иметь битовый флаг, который помечает строки как удаленные, и задание агента, выполняемое по расписанию для перемещения их в таблицу журнала. Вы можете встроить проверки заданий агента, чтобы убедиться, что схема таблицы соответствует, и задание просто не будет выполнено, если с этим шагом возникнет проблема, пока он не будет исправлен.
Таннер
Я обычно согласен с тем, SELECT *чтобы быть ленивым, но, поскольку у вас есть законная причина использовать его, он скорее серый, чем черно-белый. Вы должны попытаться сделать что-то вроде этого , но настроить его таким образом, чтобы не только иметь одинаковое количество столбцов, но и чтобы имена столбцов и типы данных были одинаковыми (поскольку кто-то может изменить типы данных и при этом вызвать проблемы в БД, которые обычно не перехватываются с вашей SELECT *«сеткой безопасности».
Daevin
3
Мне нравится идея использования SELECT *в качестве защитной сетки, но она не охватит все случаи. Например, если вы уроните столбец и добавите его снова. Это изменит порядок столбцов и (если все столбцы не относятся к одному и тому же типу) вставки в таблицу аудита завершатся неудачно или приведут к потере данных из-за неявных преобразований типов.
ypercubeᵀᴹ
2
Мне также интересно, как ваш дизайн аудита сработает, когда столбец будет удален из таблицы. Вы также удаляете столбец из таблицы аудита (и теряете все предыдущие данные аудита)?
ypercubeᵀᴹ

Ответы:

10

Как правило, это считается «ленивым» программированием.

Учитывая то, что вы специально вставляете два значения в вашу TestAuditтаблицу, я буду осторожен, чтобы ваш выбор также получал ровно два значения. Потому что, если по какой-то причине эта Testтаблица имеет или когда-либо получает третий столбец, этот триггер завершится ошибкой.

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

  • отслеживать действие, которое вы проверяете (в этом случае удалите вставки или обновления)
  • столбец даты / времени, чтобы отслеживать, когда произошло событие аудита
  • столбец идентификатора пользователя, чтобы отслеживать, кто выполнил действие, которое вы проверяете.

Так что это приводит к запросу вроде:

INSERT dbo.TestAudit([ID], [Person], [AuditAction], [ChangedOn], [ChangedBy])
SELECT [ID], [Person], 
   'Delete', -- or a 'D' or a numeric lookup to an audit actions table...
   GetDate(), -- or SYSDATETIME() for greater precision
   SYSTEM_USER -- or some other value for WHO made the deletion
FROM deleted

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

СаМ
источник
"идентификатор пользователя" Этот хитрый процесс с аудитом. Как правило, учетные записи базы данных не соответствуют реальным пользователям. Гораздо чаще они соответствуют одному веб-приложению или компоненту другого типа с одним набором учетных данных, используемых этим компонентом. (И иногда компоненты также совместно используют учетные данные.) Таким образом, учетные данные базы данных довольно бесполезны в качестве идентификатора того, кто что сделал, если вы просто не заинтересованы в том, какой компонент это сделал. Но, насколько мне известно, передавать данные приложения, которые идентифицируют «кто», не так просто с помощью функции триггера.
jpmc26
см. обновление к вопросу.
17
Другая проблема, которая может возникнуть с SELECT * в целом (хотя, вероятно, не в вашем примере), заключается в том, что если столбцы базовой таблицы не в том же порядке, что и столбцы вставки, вставка завершится неудачно.
СаМи
3

Я прокомментировал это по вашему вопросу, но решил, что постараюсь представить решение кода.

Я обычно согласен с SELECT * чтобы быть ленивым, но, поскольку у вас есть законная причина использовать его, он скорее серый, чем черно-белый.

То, что вы должны (на мой взгляд) попытаться сделать, - это что-то вроде этого , но отрегулируйте его так, чтобы имена столбцов и типы данных были одинаковыми (поскольку кто-то может изменить типы данных и при этом вызвать проблемы в БД, которые обычно не обнаруживаются вашимSELECT * безопасностью). сеть'.

Вы даже можете создать функцию, которая позволит вам быстро проверить, соответствует ли версия Audit таблицы не-Audit версии:

-- The lengths are, I believe, max values for the corresponding db objects. If I'm wrong, someone please correct me
CREATE FUNCTION TableMappingComparer(
    @TableCatalog VARCHAR(85) = NULL,
    @TableSchema VARCHAR(32) = NULL,
    @TableName VARCHAR(128) = NULL) RETURNS BIT
AS
BEGIN
    DECLARE @ReturnValue BIT = NULL;
    DECLARE @VaryingColumns INT = NULL;

    IF (@TableCatalog IS NOT NULL
            AND @TableSchema IS NOT NULL
            AND @TableName IS NOT NULL)
        SELECT @VaryingColumns = COUNT(COLUMN_NAME)
            FROM (SELECT COLUMN_NAME,
                        DATA_TYPE -- Add more columns that you want to ensure are identical
                    FROM INFORMATION_SCHEMA.COLUMNS
                    WHERE TABLE_CATALOG = @TableCatalog
                        AND TABLE_SCHEMA = @TableSchema
                        AND TABLE_NAME = @TableName
                EXCEPT
                    SELECT COLUMN_NAME,
                            DATA_TYPE -- Add more columns that you want to ensure are identical
                        FROM INFORMATION_SCHEMA.COLUMNS
                        WHERE (TABLE_CATALOG = @TableCatalog
                            AND TABLE_SCHEMA = @TableSchema
                            AND TABLE_NAME = @TableName + 'Audit')
                            AND (COLUMN_NAME != 'exclude your audit table specific columns')) adt;
    IF @VaryingColumns = 0
        SET @ReturnValue = 1
    ELSE IF @VaryingColumns > 0
        SET @ReturnValue = 0

    RETURN @ReturnValue;
END;

SELECT ... EXCEPT SELECT ...AuditПокажет вам , какие столбцы в таблице , а не в таблице аудита. Вы даже можете изменить функцию, чтобы она возвращала имена столбцов, которые не совпадали, а не просто отображали ли они или нет, или даже вызывали исключение.

Затем вы можете запустить это, прежде чем переходить DEVк PRODUCTIONсерверам для каждой таблицы в БД, используя курсор над:

SELECT TABLE_NAME
    FROM INFORMATION_SCHEMA.TABLES
    WHERE NOT (TABLE_NAME LIKE '%Audit')
Daevin
источник
1
см. обновление к вопросу
pacreely
Рад, что смог помочь. Благодарим вас за то, что вы прочитали все ответы и вернули их вашей команде для предложений; технологичность и готовность к совершенствованию - вот способы, которыми технические отделы обеспечивают бесперебойную работу компаний! : D
Daevin
0

Оператор, который вызовет триггер, потерпит неудачу, и триггер потерпит неудачу. Лучше было бы документировать триггер и контрольный журнал, чтобы вы знали, как изменить запрос, добавив столбцы вместо указания *.

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

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

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

Shaulinator
источник
0

Если ваша исходная структура или структура таблицы аудита вообще изменится, вы гарантированно столкнетесь с проблемой выбора *.

INSERT INTO [AuditTable]
(Col1,Col2,Col3)
SELECT * 
FROM [OrigTable] or [deleted];

Если что-то изменится, триггер выдаст ошибку.

Вы могли бы сделать:

INSERT INTO [AuditTable]
SELECT * 
FROM [OrigTable];

Но, как говорит CM_Dayton, это ленивое программирование и открывает двери для других несоответствий. Чтобы этот сценарий работал, вы должны быть абсолютно уверены, что вы обновляете структуру обеих таблиц одновременно.

MguerraTorres
источник