views:

1744

answers:

2

For several tables that have identity fields, we are implementing a Row Level Security scheme using Views and Instead Of triggers on those views. Here is a simplified example structure:

-- Table
CREATE TABLE tblItem (
    ItemId int identity(1,1) primary key,
    Name varchar(20)
)
go

-- View
CREATE VIEW vwItem 
AS
    SELECT *
    FROM tblItem
    -- RLS Filtering Condition
go

-- Instead Of Insert Trigger
CREATE TRIGGER IO_vwItem_Insert ON vwItem
INSTEAD OF INSERT
AS BEGIN
    -- RLS Security Checks on inserted Table

    -- Insert Records Into Table
    INSERT INTO tblItem (Name)
    SELECT Name
    FROM inserted;
END
go

If I want to insert a record and get its identity, before implementing the RLS Instead Of trigger, I used:

DECLARE @ItemId int;

INSERT INTO tblItem (Name)
VALUES ('MyName');

SELECT @ItemId = SCOPE_IDENTITY();

With the trigger, SCOPE_IDENTITY() no longer works - it returns NULL. I've seen suggestions for using the OUTPUT clause to get the identity back, but I can't seem to get it to work the way I need it to. If I put the OUTPUT clause on the view insert, nothing is ever entered into it.

-- Nothing is added to @ItemIds
DECLARE @ItemIds TABLE (ItemId int);

INSERT INTO vwItem (Name)
OUTPUT INSERTED.ItemId INTO @ItemIds
VALUES ('MyName');

If I put the OUTPUT clause in the trigger on the INSERT statement, the trigger returns the table (I can view it from SQL Management Studio). I can't seem to capture it in the calling code; either by using an OUTPUT clause on that call or using a SELECT * FROM ().

-- Modified Instead Of Insert Trigger w/ Output
CREATE TRIGGER IO_vwItem_Insert ON vwItem
INSTEAD OF INSERT
AS BEGIN
    -- RLS Security Checks on inserted Table

    -- Insert Records Into Table
    INSERT INTO tblItem (Name)
    OUTPUT INSERTED.ItemId
    SELECT Name
    FROM inserted;
END
go

-- Calling Code
INSERT INTO vwItem (Name)
VALUES ('MyName');

The only thing I can think of is to use the IDENT_CURRENT() function. Since that doesn't operate in the current scope, there's an issue of concurrent users inserting at the same time and messing it up. If the entire operation is wrapped in a transaction, would that prevent the concurrency issue?

BEGIN TRANSACTION

DECLARE @ItemId int;

INSERT INTO tblItem (Name)
VALUES ('MyName');

SELECT @ItemId = IDENT_CURRENT('tblItem');

COMMIT TRANSACTION

Does anyone have any suggestions on how to do this better?

I know people out there who will read this and say "Triggers are EVIL, don't use them!" While I appreciate your convictions, please don't offer that "suggestion".

+1  A: 

You could try SET CONTEXT_INFO from the trigger to be read by CONTEXT_INFO() in the client.

We use it the other way to pass info into the trigger but would work in reverse.

gbn
See my related question about CONTEXT_INFO() use: http://stackoverflow.com/questions/1616229/contextinfo-and-convert
Rob Garrison
@Rob: I added an answer to this
gbn
+1  A: 

Have you in this case tried @@identity? You mentioned both scope_Identity() and identity_current() but not @@identity.

HLGEM
Good thinking. The usual scoping "problem" might be a help in this case.
gbn
How would @@IDENTITY be better than IDENT_CURRENT()? From what I understand, while neither are limited to the scope of the calling code, @@IDENTITY is the last identity value inserted no matter where. So if I have an audit trigger on the table which inserts a record into an audit table, @@IDENTITY could return that row's identity (if I understand correctly). That's why I figured IDENT_CURRENT() was better because it at least limited the "scope" to the specific table.
CuppM
@CuppM: @@IDENTITY is per session, not per scope. IDENT_CURRENT() is neither and could by any session/scope
gbn
@gbn: Ah. So with the audit trigger, would @@IDENTITY return the audit record's identity?
CuppM
@CuppM: In theory. I'd like to say my CONTEXT_INFO idea is better but don't think I can... :-(
gbn
If you are calling from outside the trigger, indeed @@identity would return the worng thing if you also have an audit trigger, but I think inthis context, you would get the correct value as the other trigger hasn't run yet as you can only have one instead of trigger. But you would have to test to see. Never tried to do this, so I suggested it just as a thing to try not sure that it would work. Of course you could forbid after triggers on the table and put all the logic of them into the instead of trigger.
HLGEM
Remember that none of this works if you insert more than one record at a time. I believe triggers (when used) should always be written (and tested) to work with sets.
Rob Garrison