【问题标题】:How can I write an INSTEAD OF INSERT trigger that sets one column for any table?如何编写为任何表设置一列的 INSTEAD OF INSERT 触发器?
【发布时间】:2015-12-07 22:07:39
【问题描述】:

我正在开发一个旧版应用程序,该应用程序正在扩展为在多租户配置中运行。基本架构采用旧应用程序并向每个表添加StoreID 列。然后每个租户通过一组过滤商店 id 的视图查看旧表,例如:

create view AcmeBatWings.data as 
select * from dbo.data d where d.StoreId = 99

比这要花哨一些,但这简化了问题。

现在,我可以创建一个这样的触发器

create trigger tr_Tenant_fluff on AcmeBatWings
instead of insert
as
insert into AcmeBatWings (Name, StoreId)
select i.Name, 99 from inserted i

假设一个包含 Name 和 StoreId 列的简单表。

我的问题是我有 100 多个表,如果我要遵循这种模式,我必须为每个表创建一个专门的触发器,列出每个表的所有字段。这不仅在短期内令人讨厌,而且是维护的噩梦,因为任何表更改都需要包括触发器修改。

那么,对于任何具有 StoreId 的表,如何编写一个只在每次插入或更新时说的触发器将 StoreId 字段设置为 99?

感谢您帮助 SQL 新手!

【问题讨论】:

  • 你提到系统是多租户的;您是否计划为所有触发器硬编码值“99”,或者 StoreId 是否取决于谁在使用系统?另外,什么版本的 SQL Server?
  • 你是说每个租户对同一个基表有不同的视图,唯一的区别是他们有不同的 id 过滤器?
  • 视图和触发器在您的示例中未对齐。在视图中,AcmeBatWings 是一个模式名称。在触发器中,它是一个对象名称。它是哪一个?我假设是一个模式名称。这有很大的不同。
  • @Martin Smith - 这就是门票。这是msdn.microsoft.com/en-us/library/aa479086.aspx中讨论的第三个模型

标签: sql sql-server


【解决方案1】:

因此,您似乎在使用多个模式来传达商店信息,同时保持对象名称一致,每个商店使用一个模式,是吗?以及某种连接/用户魔法,以便查询获得正确的视图。

如果是这样,我将介绍两个令人震惊的技巧和一个推荐的解决方案(以便您了解自己的选择)。

Egregious hack #1,假设商店视图包括基表中的所有列:

CREATE TRIGGER tr_Tenant_fluff ON AcmeBatWings.data
INSTEAD OF INSERT 
AS BEGIN 
  DECLARE @StoreId INT

  SELECT @StoreId = StoreId FROM dbo.StoreSchemas 
  WHERE StoreSchema = OBJECT_SCHEMA_NAME(@@PROCID)

  INSERT dbo.data SELECT *, @StoreId FROM inserted
END

如果您曾经向基表添加一列,则必须更新所有商店视图以包含该列,否则触发器将中断。

Egregious hack #2,假设与 (1) 相同,只是 StoreId 包含在商店视图中:

CREATE TRIGGER tr_Tenant_fluff ON AcmeBatWings.data
INSTEAD OF INSERT 
AS BEGIN 
  DECLARE @StoreId INT

  SELECT @StoreId = StoreId FROM dbo.StoreSchemas 
  WHERE StoreSchema = OBJECT_SCHEMA_NAME(@@PROCID)

  SELECT * INTO #inserted FROM inserted
  UPDATE #inserted SET StoreId = @StoreId

  INSERT dbo.data SELECT * FROM #inserted
END

hack #2 相对于hack #1 的好处是您可以使用SELECT * 定义您的商店视图,如果基表发生更改,您只需使用sp_refreshview 重新编译所有商店视图。缺点是您将插入的数据从一个中间表复制到另一个中间表,并更新第二个表。这使您的INSTEAD OF INSERT 触发器的开销增加了三倍,这在开始时已经相当昂贵。即,

  • INSTEAD OF INSERT 触发器的基本开销 -> 填充 inserted 的成本 -> x
  • inserted 填充#inserted 的成本-> 大约x
  • 更新成本#inserted -> 关于x
  • 令人震惊的 hack #2 的总开销:大约 3x

否则,最好的办法是编写触发器脚本。这是一个相当简单的过程,一旦您熟悉了系统表,您就可以调整您认为合适的触发器生成。就此而言,您还应该编写商店视图的脚本。

让您开始:

CREATE TABLE dbo.data (Name VARCHAR(10), StoreId INT)
GO
CREATE SCHEMA StoreA
GO
CREATE SCHEMA StoreB
GO
CREATE SCHEMA StoreC
GO
CREATE VIEW StoreA.data AS SELECT Name FROM dbo.data WHERE StoreId = 1
GO
CREATE VIEW StoreB.data AS SELECT Name FROM dbo.data WHERE StoreId = 2
GO
CREATE VIEW StoreC.data AS SELECT Name FROM dbo.data WHERE StoreId = 3
GO
CREATE TABLE dbo.StoreSchemas (StoreSchema SYSNAME UNIQUE, StoreId INT PRIMARY KEY)
GO
INSERT dbo.StoreSchemas VALUES ('StoreA', 1), ('StoreB', 2), ('StoreC', 3)
GO

DECLARE @crlf NCHAR(2) = NCHAR(13)+NCHAR(10)
SELECT
  N'CREATE TRIGGER tr_Tenent_fluff ON '+schema_name(v.schema_id)+N'.data'+@crlf
+ N'INSTEAD OF INSERT'+@crlf
+ N'AS BEGIN'+@crlf
+ N'  INSERT dbo.data ('
+ STUFF((
    SELECT @crlf+N'  , '+name FROM sys.columns tc 
    WHERE tc.object_id = t.object_id
      AND (tc.name IN (SELECT name FROM sys.columns vc WHERE vc.object_id = v.object_id)
        OR tc.name = N'StoreId')
    ORDER BY tc.column_id
    FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)')
    ,5,1,N' ')+@crlf
+ N'  )'+@crlf
+ N'  SELECT'
+ STUFF((
    SELECT @crlf+N'  , '+name
      + CASE WHEN name = N'StoreId' THEN ' = '+(
          SELECT CONVERT(NVARCHAR,StoreId) FROM dbo.StoreSchemas s 
          WHERE s.StoreSchema = SCHEMA_NAME(v.schema_id)
          )
        ELSE '' END
    FROM sys.columns tc 
    WHERE tc.object_id = t.object_id
      AND (tc.name IN (SELECT name FROM sys.columns vc WHERE vc.object_id = v.object_id)
        OR tc.name = N'StoreId')
    ORDER BY tc.column_id
    FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)')
    ,5,1,N' ')+@crlf
+ N'  FROM inserted'+@crlf
+ N'END'+@crlf
+ N'GO'+@crlf
FROM sys.tables t 
JOIN sys.views v 
  ON t.name = v.name 
 AND t.schema_id = SCHEMA_ID('dbo') 
 AND v.schema_id <> t.schema_id
WHERE t.name = 'data'
GO

【讨论】:

  • 噢,我喜欢脚本部分。我现在要睡觉了,但我会在早上仔细看看。
【解决方案2】:

所以,如果我没看错,每个商店都有自己的 ID。将数据库部署到每个商店,并且数据库应根据其部署位置以最少的代码工作记录不同的 StoreId。这是我的建议。在数据库中创建一个表来保存 StoreId。创建一个函数来检索该 StoreId。然后在每个表中创建 StoreId 列作为使用该函数的计算列。因此,在每次部署中,唯一的变化是更新一个表中的 StoreId。比如:

/* This table is updated with the unique value for each individual store */
create table MyStore (
    StoreId int
)

insert into MyStore
    (StoreId)
    values
    (99)        
go

/* This function will be used in the computed column of each table */
create function dbo.LookupStoreId()
returns int
as
begin
    return (select StoreId from MyStore)
end
go

create table AcmeBatWings (
    Name char(10),
    StoreId as dbo.LookupStoreId()
) 

insert into AcmeBatWings
    (Name)
    values
    ('abcde')

select Name, StoreId from AcmeBatWings
go

/* Clean up after demo */
drop table AcmeBatWings
drop table MyStore
drop function dbo.LookupStoreId
go

【讨论】:

  • 此解决方案的缺点:
【解决方案3】:

为什么不使用触发器来更新每个表,将StoreId NOT NULL 设为默认值 99?

根据说明进行编辑

您可以尝试使用 AFTER INSERT, UPDATE 触发器来替代 INSTEAD OF 触发器

create trigger tr_Tenant_fluff on AcmeBatWings
AFTER insert, update
as

-- You'll need to get @StoreID here somehow

update AcmeBatWings 
set StoreID = @StoreID
where [Name] IN (SELECT [Name] FROM inserted) -- update based on primary key

虽然这会更新您刚刚插入或更新的数据,但它确实具有在您在表中添加或删除列时不会中断的好处。

【讨论】:

  • @Martin Smith - 这就是我想知道的,但该示例使用了硬编码的“99”,所以这让我感到困惑。
  • 是的,我删除了那条评论,因为我自己并不确定。
  • 商店ID不会绑定到视图,而不是基表吗?在这种情况下,默认基表中的列不会解决问题。
  • @Peter - 你是对的,这是我的误解
  • 组合您的解决方案:使基表中的 StoreId 不为空,默认为 -1。然后,在AFTER 触发器中,将-1 更新为@StoreId,无需加入。我想测试不同客户端的并发插入,但我相信触发器会将它们序列化。我认为这不会比INSTEAD OF INSERT 触发器差多少,它只需要在插入之后,而不是插入和更新。
【解决方案4】:

偶然发现了这个老问题:

您可以为 StoreId 创建一个默认约束(通过选择 sys.columns 查找具有 StoreId 列的所有表),可以是固定的 99,也可以是在另一个表中查找的函数或返回固定 99 的函数(这样在将数据库移动到另一个商店时,您只需更改一个函数而不是 100 个约束)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多