views:

40

answers:

3

My application involves using submitting data (the "request") from a form into an SQL Server 2005 database, for later review and approval by a supervisor. Users should have permission to insert a new request, but not be able to modify the ones they have already submitted.

With a single table, this is straightforward: grant them the INSERT privilege only without the UPDATE privilege. But the request actually spans two tables, with a one-to-many relationship. I need to prevent the user from inserting additional child rows for an existing request. Ideally, this should be enforced at the database level: allow a parent row and one or more child rows to be inserted in the same transaction, but once that transaction is committed prevent new child rows from being inserted with that foreign key.

What's the best way to achieve this? Are there any ways to enforce this special flavour of "referential integrity" without triggers? And if triggers are they only way, then how can I test that the parent row has been inserted within the current transaction?

+3  A: 

Stored procedure or trigger

If you grant rights on the child table, then they'll be able to write.

A trigger will disallow the write and a stored procedure allows you to prevent the write in the first place because only the stored proc writes to the tables.

There is no "native" referential integrity that can capture your business logic because it's custom to your situation.

gbn
Yes, that sounds reasonable (using the "EXECUTE AS OWNER" clause on the stored procedure or trigger).
Todd Owen
No need for EXECUTE AS OWNER though
gbn
+1  A: 

Use stored procedures to insert data:

  • first stored procedure inserts parent row; it automatically adds information about inserting user and sets status field,

  • second stored procedure inserts child rows after checking its parent row; it raises error if calling user has no right to add items to given parent row or parent row's status disallows adding new positions.

Alternatively you can use triggers to do the checks. But this can be somewhat trickier than explicitly calling stored procedures. And people sometimes tend to forget about triggers.

Tomek Szpakowicz
Hmm, probably more complex/flexible than I need. Since all the data is inserted at the same time (same transaction), I could just pass all the data to a single stored procedure which writes to both tables, I think.
Todd Owen
@Todd Owen: You can use single stored procedure or a view with instead of insert trigger or Ed Harper's solution. Provided you need to insert one child row for one parent row (why are there two tables?). Otherwise, you'll need something more flexible.
Tomek Szpakowicz
+1  A: 

The following example illustrates how you could use a trigger to achieve this behaviour. Note that this will not work if the child rows are inserted one at a time inside the transaction, rather than in a single INSERT statement.

CREATE TABLE parent1
(id INT PRIMARY KEY)

CREATE TABLE child1
(id INT
,parent_id INT
)
GO

ALTER TABLE child1 ADD CONSTRAINT chilld1fk FOREIGN KEY (parent_id)
REFERENCES parent1 (id)
GO


CREATE TRIGGER trg_child1
ON child1
INSTEAD OF INSERT
AS

        SELECT parent_id
        FROM child1 AS c
        WHERE EXISTS (SELECT 1
                      FROM inserted AS i
                      WHERE i.parent_id = c.parent_id
                     )

        IF @@ROWCOUNT > 0
            BEGIN
                RAISERROR('You cannot amend this request',16,1)
            END
        ELSE        
            BEGIN
                INSERT child1
                SELECT id
                       ,parent_id
                FROM inserted
            END
GO                                                            

BEGIN TRAN
        INSERT parent1
        VALUES (1)

        INSERT child1 
        (id
        ,parent_id
        )
        SELECT 10,1
        UNION SELECT 11,1        
COMMIT

-- attempting to insert another child outside the transaction
-- will result in an error
INSERT child1
SELECT 12,1

SELECT * FROM child1
Ed Harper
Thanks. This is interesting because I am hoping to using InfoPath on the client side, and your approach *might* mean I can do that without any custom code behind the submit button.
Todd Owen