views:

56

answers:

4

Hi!

Long time reader, first time poster ;-)

I'm implementing a system based on an old system. The new system uses SQL Server 2008 and my problem comes when trying to insert new items in the main table. This will happen in two ways: It may be imported from the existing system (1) or may be created in the new system (2).

In case (1) the item already has an ID (int) which I would like to keep. In case (2) the ID will not be filled in and I'd like to generate an ID which is +1 of the maximum current value in the table. This should of course also work for inserts of mutiple rows.

As far as I can see, the solution will be to create a INSTEAD OF TRIGGER, but I can't quite figure out how this is done. Can anyone give me a hint or point me in the direction of how this can be done?

Chris

A: 

Undeleted

Messing around with identity columns and setting IDENTITY_INSERT on and off dependant upon whether you are inserting a new or old item doesn't seem that great both from a permissions and a concurrency POV. I suggest an approach and hopefully others will identify improvements

Create a single row, single column table to track your own Ids

--Table to hold single record with value of highest allocated "new" id
CREATE TABLE [dbo].[YourTableMaxId](
    [Lock] char(1) DEFAULT 'X' NOT NULL PRIMARY KEY CHECK (lock = 'X'),
    [MaxId] [int] NOT NULL
)

--Initially populated with some high value
insert into [dbo].[YourTableMaxId](MaxId) VALUES (1000000)

The INSTEAD OF trigger (untested but to show an approach)

CREATE TRIGGER [dbo].[tr_InsteadOfInsert] 
   ON  [dbo].[YourTable] 
   INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON;


INSERT INTO [dbo].[YourTable]
           ([Id]
          ,... Other Columns)
SELECT [Id]
      ,... Other Columns
  FROM INSERTED WHERE ([Id] IS NOT NULL)

DECLARE @NumberOfNewRecords INT

SELECT @NumberOfNewRecords = COUNT(*) FROM INSERTED
WHERE ([Id] IS NULL)

    IF (@NumberOfNewRecords > 0)
    BEGIN
        DECLARE @MaxId TABLE (MaxId INT)

        --Atomic Reservation of Sequence Numbers and output of start of sequence
        UPDATE [dbo].[YourTableMaxId]
        SET [MaxId] = [MaxId] + @NumberOfNewRecords
        OUTPUT DELETED.MaxId INTO @MaxId

        INSERT INTO [dbo].[YourTable]
           ([Id]
          ,... Other Columns)
        SELECT ROW_NUMBER() OVER (ORDER BY [Id]) + (SELECT MaxId FROM @MaxId) AS Id ,... Other Columns
        FROM INSERTED WHERE ([Id] IS NULL)
    END

END
Martin Smith
All I have access to at insert time, are the values to be inserted - I don't have access to the old database. Is still MERGE the solution? If so, I don't quite see how. Have you got an example of how this might be done?
Chris Ridge
@Chris- instead of inserting directly into your new table you can put the rows into a staging table. At that point you should have access to both old and new records and can MERGE the two.
Mike M.
@Chris - Probably. The source for the Merge statement can be a table variable for example. I'll knock up a quick example.
Martin Smith
I must admit that I'm not very used to the MERGE statement, so I might be missing something obvious here - but to me that seems to be a longer way to the objective. At least if my suggested solution is achievable. I take it you're talking about making inserts through a stored procedure which uses a MERGE statement? Wouldn't using a trigger save me the hassle of having to remember to use the insert SP whenever I need to insert an item? There will be inserts to this table from a LOT of sources... In my head the trigger approach is a fire and forget strategy - and the appeals to me :-)
Chris Ridge
+1  A: 

How about using a stored procedure to do your inserts, with the primary key as an optional parameter. In the stored procedure, you can set the primary key when it is not passed.

I would caution that if old and new records are being inserted mix and match, your scenario will probably fail, as new records will be getting old ID's before the old records are inserted. I recommend getting the max ID of the old table right now, and in the stored procedure setting the new primary key to be the Greater value of (old max + 1, current table max)

Fosco
I see that option as a possibility, but I find the trigger solution to be a cleaner one, if it is possible to achieve that. If not, I will surely go for the stored procedure solution.Regarding the ID crash - I'm aware of the problem, and I will generate new ID's large enough for that crash never to happen (I just left that part out for simplicity). Or, at least not for another 75 years - which should suffice for a system destined for the scrap heap some time next year ;-)
Chris Ridge
I think perhaps I misunderstood your comment about old and new ID's. And I think I didn't describe the situation well enough. The old system will continue to generate new items, so the ID's from the old system will continue to rise in value even after the new system is put into production. Not until all features have been ported, will the old system be scrapped.
Chris Ridge
@Chris - Right I was just about to ask you why you couldn't import all the old records in and be done with it but that explains it.
Martin Smith
+1  A: 

Following your request of using an INSTEAD OF trigger this SQL code can get you started.

CREATE TABLE dbo.SampleTable
(
    ID INT,
    SomeOtherValue VARCHAR(100) NULL
)
GO

CREATE TRIGGER dbo.TR_SampleTable_Insert
   ON  dbo.SampleTable
   INSTEAD OF INSERT
AS 
BEGIN

    SET NOCOUNT ON;

    -- Inserting rows with IDs
    INSERT INTO dbo.SampleTable (
        ID, 
        SomeOtherValue)
    SELECT 
        ID, 
        SomeOtherValue
    FROM
        Inserted    
    WHERE
        ID IS NOT NULL

    -- Now inserting rows without IDs
    INSERT INTO dbo.SampleTable (
        ID, 
        SomeOtherValue)
    SELECT 
        (SELECT ISNULL(MAX(ID), 0) FROM dbo.SampleTable) 
            + ROW_NUMBER() OVER(ORDER BY ID DESC),
        SomeOtherValue
    FROM
        Inserted
    WHERE
        ID IS NULL

END
GO

INSERT INTO dbo.SampleTable
SELECT 1, 'First record with id'
UNION
SELECT NULL, 'First record without id'
UNION
SELECT 2, 'Second record with id'
UNION
SELECT NULL, 'Second record without id'
GO

SELECT * FROM dbo.SampleTable
GO
JCallico
I fully agree with previous comments that mixing the old IDs with the new ones is not recommended.
JCallico
Thank you - just the sort of example I was looking for :-) Regarding the mixing of IDs I see the downside - but the project also gives a few downsides to this approach not relevant to this technical issue.
Chris Ridge
Glad I was able to help. Thats why I implemented what you asked for originally instead of immediately suggesting an alternate approach just for the sake of it hopping to get points from you. It seems like a trend on StackOverflow these days.
JCallico
A: 

Others have shown you how to write such trigger.

Another, and often recommended, approach is to store both IDs in new database. Each record gets new ID in new system (by IDENTITY column or some other means). Additionally, if the record is imported from another system, it has associated OriginSystem and OriginID. This way you can keep old IDs for reference. This approach has additional benefit of being able to support new system to import data from, e.g. when merging or exchanging data with another system.

Tomek Szpakowicz
This is what I would if presented with the same problem.
JCallico
Thank you for your comment. This solution has been considered. Due to details of the project not described here, this would complicate the data model more than it may appear (the old system has other backing systems which needs to be handled too). The final solution for this problem hasn't been decided on yet - but it's nice to know that the trigger solution seems to be a possibility.
Chris Ridge