Reference a view in a foreign key

  • I know that you cannot reference a view in a foreign key. I wonder if there is an alternative for that.

    Background:

    I have a legacy DB with a lot of code relying to that DB structure. I don't want to embark on changing all the sql queries referencing the table I have to rework:

    The table in question contains products, now I want to introduce a new table (e.g. notSupportedProducts) that contains products that aren't supported anymore. That table will only contain an ID, referencing the product table.

    Now, transparent for the application, all calls to the product table need to omit the rows that are in the notSupportedProducts table.

    I thought about creating a view that does exactly that. Just rename the original table to productsxy and call the view products, so that there is no need for change in the application.

    Unfortunately I have a couple of functions, procedures, triggers and tables (foreign keys) that refer to that products table, so that idea got stuck. Has anyone an idea, getting around that limitation? Or an alternative that is (almost) transparent to the application?

    Cheers

    Dankwart

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • A view named dbo.products with three INSTEAD OF triggers on it (one for INSERT, one for UPDATE and one for DELETE) may be what you are after.

    Here is a code sample that demonstrates the technique showing how the INSTEAD OF INSERT trigger would work.

    USE tempdb

    GO

    IF OBJECT_ID(N'dbo.notSupportedProducts') IS NOT NULL

    DROP TABLE dbo.notSupportedProducts

    GO

    IF OBJECT_ID(N'dbo.productsxy') IS NOT NULL

    DROP TABLE dbo.productsxy

    GO

    CREATE TABLE dbo.productsxy

    (

    id INT PRIMARY KEY ,

    name VARCHAR(100)

    ) ;

    CREATE TABLE dbo.notSupportedProducts ( id INT ) ;

    GO

    ALTER TABLE dbo.notSupportedProducts ADD CONSTRAINT [fk_productsxy] FOREIGN KEY (id) REFERENCES dbo.productsxy(id) ;

    GO

    IF OBJECT_ID(N'dbo.products') IS NOT NULL

    DROP VIEW dbo.products

    GO

    CREATE VIEW dbo.products

    AS

    SELECT *

    FROM dbo.productsxy ;

    GO

    CREATE TRIGGER dbo.products_trg_instead_insert ON dbo.products

    INSTEAD OF INSERT

    AS

    BEGIN

    INSERT INTO dbo.productsxy

    ( id, name )

    SELECT *

    FROM inserted ;

    END

    GO

    INSERT INTO dbo.products

    ( id, name )

    VALUES ( 1, 'some product' ) ;

    GO

    SELECT 'dbo.productsxy - base table' AS _object ,

    id ,

    name

    FROM dbo.productsxy

    UNION ALL

    SELECT 'dbo.products - view' ,

    id ,

    name

    FROM dbo.productsxy

    GO

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • Thanks for the snippet,

    It's about what I tried when I hit the problem that the existing references to the table dbo.product now have to reference a view in a foreign key, which is not possible in MSSQL. The only way around seems to redirect the references to the renamed table dbo.productsxy. I was hoping to achieve that without touching all the referencing tables, views, functions etc.

    If anyone has a less intrusive idea, please let me know.

    Cheers

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • dankwart menor (5/19/2011)


    Thanks for the snippet,

    It's about what I tried when I hit the problem that the existing references to the table dbo.product now have to reference a view in a foreign key, which is not possible in MSSQL. The only way around seems to redirect the references to the renamed table dbo.productsxy. I was hoping to achieve that without touching all the referencing tables, views, functions etc.

    If anyone has a less intrusive idea, please let me know.

    Cheers

    What do you mean by "redirect the references to the renamed table dbo.productsxy". When you rename the table from dbo.products to dbo.productsxy all the existing keys and key references will stay in place, which I think is a good thing as the rename will be transparent once you add the VIEW.

    Maybe this will make more sense. Try this code. I have taken the next step by redefining the VIEW a little closer to what you will want to have in place and by adding more example data it will become more clear:

    USE tempdb

    GO

    IF OBJECT_ID(N'dbo.notSupportedProducts') IS NOT NULL

    DROP TABLE dbo.notSupportedProducts

    GO

    IF OBJECT_ID(N'dbo.productsxy') IS NOT NULL

    DROP TABLE dbo.productsxy

    GO

    CREATE TABLE dbo.productsxy

    (

    id INT PRIMARY KEY ,

    name VARCHAR(100)

    ) ;

    CREATE TABLE dbo.notSupportedProducts ( id INT ) ;

    GO

    ALTER TABLE dbo.notSupportedProducts ADD CONSTRAINT [fk_productsxy] FOREIGN KEY (id) REFERENCES dbo.productsxy(id) ;

    GO

    IF OBJECT_ID(N'dbo.products') IS NOT NULL

    DROP VIEW dbo.products

    GO

    CREATE VIEW dbo.products

    AS

    SELECT p.*

    FROM dbo.productsxy p

    LEFT JOIN dbo.notSupportedProducts nsp ON p.id = nsp.id

    WHERE nsp.id IS NULL ;

    GO

    CREATE TRIGGER dbo.products_trg_instead_insert ON dbo.products

    INSTEAD OF INSERT

    AS

    BEGIN

    INSERT INTO dbo.productsxy

    ( id, name )

    SELECT *

    FROM inserted ;

    END

    GO

    INSERT INTO dbo.products

    ( id, name )

    VALUES ( 1, 'some product' ),

    ( 2, 'some other product' ) ;

    GO

    INSERT INTO dbo.notSupportedProducts

    ( id )

    VALUES ( 1 ) ;

    GO

    SELECT 'dbo.productsxy - base table' AS _object ,

    id ,

    name

    FROM dbo.productsxy

    UNION ALL

    SELECT 'dbo.products - view' ,

    id ,

    name

    FROM dbo.products

    GO

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • What I mean is that there are tables that reference dbo.products via foreign key at the moment. If I rename the table (using sp_rename), the foreign keys will still point to dbo.products, won't they? And as that is a view now, it will crash.

    is there a reference-aware way to rename?

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • What would the 'instead of update' and 'instead of delete' trigger look like? Insert is pretty straight forward...

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • dankwart menor (5/19/2011)


    What I mean is that there are tables that reference dbo.products via foreign key at the moment. If I rename the table (using sp_rename), the foreign keys will still point to dbo.products, won't they?

    No, they will continue to point to the original object with the new name, i.e. dbo.productsxy will still be the table referenced by the foreign keys after the rename. Creating a new VIEW after the rename called dbo.products will not affect dbo.productsxy or any of its keys.

    is there a reference-aware way to rename?

    Renames do not affect keys or key references, it is a data-dictionary name change only, so references are unaffected.

    What would the 'instead of update' and 'instead of delete' trigger look like? Insert is pretty straight forward...

    In the INSTEAD OF UPDATE trigger you would do an UPDATE...JOIN between the inserted table and dbo.productsxy on the primary key to update any affected rows in the base table. Something like:

    UPDATE p

    SET name = i.name,

    some_other_field = i.some_other_field

    FROM inserted i

    -- JOIN always works assuming id is your PK and that no one ever updates that

    -- field...if the PK is ever updated it gets more complicated as you have to

    -- involve the deleted table too

    JOIN dbo.productsxy p on i.id = p.id ;

    Same principals used in the INSTEAD OF UPDATE trigger holds true for the INSTEAD OF DELETE trigger. If you get stuck building those two triggers post what you have and I or someone will help you along.

    Edit: PS if you do post trigger code down the line please be sure to also post your table definitions and some sample data to support remote development efforts on this side 😀

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • Thanks, mate

    I will work on that.

    Cheers

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • If I do it like that:

    UPDATE p

    SET name = i.name,

    some_other_field = i.some_other_field

    FROM inserted i

    JOIN dbo.productsxy p on i.id = p.id ;

    And include every single field that exists in the table p, what happens to the some_other_field field if I say

    UPDATE products SET name = "test"

    Will it stay untouched? Or will it be overriden by null or the default value? Is that what the JOIN is for?

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • dankwart menor (5/22/2011)


    If I do it like that:

    UPDATE p

    SET name = i.name,

    some_other_field = i.some_other_field

    FROM inserted i

    JOIN dbo.productsxy p on i.id = p.id ;

    And include every single field that exists in the table p, what happens to the some_other_field field if I say

    UPDATE products SET name = "test"

    Will it stay untouched? Or will it be overriden by null or the default value? Is that what the JOIN is for?

    You want to look at the UPDATE() function in SQL Server. The function is useful in the context of an update trigger to determine whether the caller explicitly supplied a value for a particular field that exists in the inserted table.

    Your trigger can contain a single UPDATE JOIN statement that addresses all columns in the table and conditionally updates each field depending on whether the caller supplied a value. It might look something like this:

    UPDATE p

    SET name = CASE WHEN UPDATE(name) = 1 THEN i.name

    ELSE p.name

    END,

    some_other_field = CASE WHEN UPDATE(some_other_field) = 1 THEN i.some_other_field

    ELSE p.some_other_field

    END

    FROM inserted i

    JOIN dbo.productsxy p ON i.id = p.id ;

    Basically this is saying for each column "if the caller supplied a value for the column then update the table with it, otherwise update the table with what was already there (i.e. no effective data change)".

    Is that what the JOIN is for?

    You're almost there! There is a distinct data-flow in play here that you need to understand. If you got this part great, but I want to make sure, so here it is restated:

    When the caller updates the VIEW dbo.products no data changes are actually taking place because you have the INSTEAD OF INSERT trigger in place. The only thing that happens via the issuance of the UPDATE statement from the caller is that the virtual inserted and deleted tables are populated with the data changes that would take place on the data exposed by the VIEW if the VIEW were updatable. It is up to you, in the context of the INSTEAD OF trigger, to take those data changes from the virtual tables and do something useful with them. The UPDATE...JOIN bridges the gap between the virtual tables available inside the trigger and the concrete table dbo.productsxy.

    Edit: modified code sample, UPDATE() returns bit so must say "WHEN UPDATE(name) = 1 THEN" instead of just "WHEN UPDATE(name) THEN"

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • Thanks, that is working exquisitely

    --------------------------------------------------------
    [font="Comic Sans MS"]Correct me if I'm wrong[/font]

  • dankwart menor (5/23/2011)


    Thanks, that is working exquisitely

    Excellent! I love it when a plan comes together 😀

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

  • opc.three rocks it again!

  • Thanks for the nod Yin :Wow:

    There are no special teachers of virtue, because virtue is taught by the whole community.
    --Plato

Viewing 14 posts - 1 through 13 (of 13 total)

You must be logged in to reply to this topic. Login to reply