1

I'm attempting to track items in commercial building construction (specifically concrete cladding).

  • A project can have multiple blocks (a block being a single standing building)
  • A block can have multiple elevations (building face; north, east, etc)
  • An elevation can have many floors (a tall building)
  • A floor can have many panels (a panel being a section of concrete cladding)

To make things easier when building the panels in the database so they can be tracked, I want to be able copy a block (since 9 times out of 10, each block is the same with minor changes)

As far as tables go -

Blocks

Blocks.BlockID, 
Blocks.BlockName, 
Blocks.BlockDescription, 
Projects.ProjectID

Elevations

Elevations.ElevationID, 
Elevations.ElevationName, 
Elevations.ElevationDescription, 
Blocks.BlockID

Floors

Floors.FloorID, 
Floors.FloorName, 
Floors.FloorDescription, 
Elevations.ElevationID

Panels

Panels.PanelID, 
Panels.PanelName, 
Panels.PanelDescription, 
Floors.FloorID

If I copy a block it needs to

  1. Copy the current block, but the user should be able to choose Blocks.name.
  2. Copy all of the elevations on that block
  3. Copy all floors on each of those elevations
  4. Copy all panels on each of those floors

Let me know if you need to know anything else, thanks for any help in advance!

My Attempt (after previous advice)

DECLARE @blockToCopy int = 1
DECLARE @BlockOutput table
( BlockID int);
DECLARE @ElevationsOutput table
(  ElevationID int, BlockID int );
DECLARE @FloorsOutput table
(  FloorID int, ElevationID int );
DECLARE @ItemsOutput table
(  ItemID int, FloorID int );

INSERT INTO Blocks
(ProjectID,BlockName,BlockDescription) 
    OUTPUT 
    INSERTED.BlockID
    INTO @BlockOutput
SELECT ProjectID,'NewNameTest','NewDescTest' 
from Blocks
WHERE BlockID=@blockToCopy

INSERT INTO Elevations
(BlockID,ElevationName,ElevationDescription) 
    OUTPUT 
    INSERTED.ElevationID,
    INSERTED.BlockID
    INTO @ElevationsOutput
SELECT (SELECT BlockID from @BlockOutput),ElevationName,ElevationDescription 
from Elevations
WHERE BlockID=@blockToCopy

INSERT INTO Floors
(ElevationID,FloorName,FloorDescription)
    OUTPUT 
    INSERTED.FloorID,
    INSERTED.ElevationID
    INTO @FloorsOutput
SELECT (SELECT top 1 ElevationID from @ElevationsOutput order by ElevationID desc), FloorName,FloorDescription 
from Floors
WHERE ElevationID in (SELECT ElevationID from @ElevationsOutput)

It goes wrong after the second cascade (where trying to copy all floors within each elevation). I know why it's going wrong (instead of listing the original IDs alongside the new IDs to copy to, I'm outputting the new IDs by themselves so it has nothing to copy because they don't exist yet) but I don't know how to fix it.

Vladimir Baranov
  • 31,799
  • 5
  • 53
  • 90
hero9989
  • 65
  • 1
  • 1
  • 11
  • Why do you need to make copies of the Elevations, Panels and Items? Couldn't the new Block still point to existing Elevations etc. – cloudsafe Jan 11 '17 at 16:28
  • 2
    SO is not a code writing service. We can help you when you get stuck but we are not going to do your job for you. How about if you try to write this and then share what you have tried and what part(s) you need help with. – Sean Lange Jan 11 '17 at 16:29
  • @MassimilianoKraus A stored procedure would be simplest to implement as far as the front end is concerned but whichever is easiest – hero9989 Jan 11 '17 at 16:37
  • @cloudsafe Because each other item has to be built and installed so each has to be uniquely identifiable. – hero9989 Jan 11 '17 at 16:37
  • @SeanLange Have added my attempt to OP with description – hero9989 Jan 11 '17 at 16:42
  • I think you would be better off starting at the top instead of the bottom. And do you want only 1 child at each level? Not sure why your insert into Items has a top 1. I would make use of some temp tables here and the OUTPUT clause. That way you start at the Block you want to copy. And get all the ElevationIDs you need to copy into a temp table in your insert. Then repeat that process down the chain of child tables. – Sean Lange Jan 11 '17 at 16:56
  • @SeanLange The top 1 just gets the floor ID from the newly created floor above. Since it's a foreign key in the Items table. I will have a google for how to use the output clause. Thanks – hero9989 Jan 12 '17 at 08:44
  • Possible duplicate of [Copy multiple records with a master-details relationship](http://stackoverflow.com/questions/27488744/copy-multiple-records-with-a-master-details-relationship) – Vladimir Baranov Jan 12 '17 at 12:53
  • @VladimirBaranov This only gets it as far as I've already gotten. It doesn't explain what to do on the 2nd or 3rd cascades. – hero9989 Jan 12 '17 at 13:59
  • That's why you need to keep a mapping between old and new IDs. That's why you need to use `MERGE` instead of simple `INSERT`. – Vladimir Baranov Jan 12 '17 at 14:22
  • 1
    @VladimirBaranov Never having used merge before. It's not the easiest task to figure out how to do it. Any chance of some guidance? :) – hero9989 Jan 12 '17 at 14:26

1 Answers1

1

I assume that Blocks.BlockID, Elevations.ElevationID, Floors.FloorID, Panels.PanelID are primary keys and autogenerated IDENTITY.

  • One Block has many Elevations.
  • One Elevation has many Floors.
  • One Floor has many Panels.

I'd use MERGE with OUTPUT clause.

MERGE can INSERT, UPDATE and DELETE rows. In this case we need only INSERT.

1=0 is always false, so the NOT MATCHED BY TARGET part is always executed. In general, there could be other branches, see docs. WHEN MATCHED is usually used to UPDATE; WHEN NOT MATCHED BY SOURCE is usually used to DELETE, but we don't need them here.

This convoluted form of MERGE is equivalent to simple INSERT, but unlike simple INSERT its OUTPUT clause allows to refer to the columns that we need. It allows to retrieve columns from both source and destination tables thus saving a mapping between old existing IDs and new IDs generated by IDENTITY.

Block

Copy one given Block and remember the ID of the new Block. We can use simple INSERT and SCOPE_IDENTITY here, because BlockID is primary key and only one row can be inserted.

DECLARE @blockToCopy int = 1;
DECLARE @VarNewBlockID int;
INSERT INTO Blocks
    (ProjectID
    ,BlockName
    ,BlockDescription)
SELECT
    ProjectID
    ,'NewNameTest'
    ,'NewDescTest'
FROM Blocks
WHERE Blocks.BlockID = @blockToCopy
;
SET @VarNewBlockID = SCOPE_IDENTITY();

Elevations

Copy Elevations from old Block and assign them to the new Block. Remember the mapping between old IDs and freshly generated IDs in @MapElevations.

DECLARE @MapElevations TABLE(OldElevationID int, NewElevationID int);

MERGE INTO Elevations
USING
(
    SELECT
        ElevationID
        ,@VarNewBlockID AS BlockID
        ,ElevationName
        ,ElevationDescription
    FROM Elevations
    WHERE Elevations.BlockID = @blockToCopy
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT
    (BlockID
    ,ElevationName
    ,ElevationDescription)
VALUES
    (Src.BlockID
    ,Src.ElevationName
    ,Src.ElevationDescription)
OUTPUT
    Src.ElevationID AS OldElevationID
    ,inserted.ElevationID AS NewElevationID
INTO @MapElevations(OldElevationID, NewElevationID)
;

Floors

Copy Floors using mapping between old and new ElevationID. Remember the mapping between old IDs and freshly generated IDs in @MapFloors.

DECLARE @MapFloors TABLE(OldFloorID int, NewFloorID int);

MERGE INTO Floors
USING
(
    SELECT
        Floors.FloorID
        ,M.NewElevationID AS ElevationID
        ,Floors.FloorName
        ,Floors.FloorDescription
    FROM
        Floors
        INNER JOIN Elevations ON Elevations.ElevationID = Floors.ElevationID
        INNER JOIN @MapElevations AS M ON M.OldElevationID = Elevations.ElevationID
    WHERE Elevations.BlockID = @blockToCopy
) AS Src
ON 1 = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT
    (ElevationID
    ,FloorName
    ,FloorDescription)
VALUES
    (Src.ElevationID
    ,Src.FloorName
    ,Src.FloorDescription)
OUTPUT
    Src.FloorID AS OldFloorID
    ,inserted.FloorID AS NewFloorID
INTO @MapFloors(OldFloorID, NewFloorID)
;

Panels

Copy Panels using mapping between old and new FloorID. This is the last level of details, so we can use simple INSERT and don't remember the mapping of IDs.

INSERT INTO Panels
    (FloorID
    ,PanelName
    ,PanelDescription)
SELECT
    M.NewFloorID
    ,Panels.PanelName
    ,Panels.PanelDescription
FROM
    Panels
    INNER JOIN Floors ON Floors.FloorID = Panels.FloorID
    INNER JOIN Elevations ON Elevations.ElevationID = Floors.ElevationID
    INNER JOIN @MapFloors AS M ON M.OldFloorID = Floors.FloorID
WHERE Elevations.BlockID = @blockToCopy
;
Vladimir Baranov
  • 31,799
  • 5
  • 53
  • 90
  • Fantastic answer! I added another Layer to it all so now a Panel can have many 'products' (e.g. windows). The Explanation made it easy to make panels into merge/output and the new final layer a simple insert! Thanks so much! – hero9989 Jan 13 '17 at 11:51