Here is a possible solution. Navigating the structure from top to bottom may be a little gross but maybe the app side objects can handle it.
I think you can build this out using 2 tables dbo.Device
and dbo.Combiner
. Each table has a column that references who its parent object is, regardless of whether it is a device
or a combiner
. We can then use a couple different types of constraints
and a couple of triggers
to ensure that the data follows the path you are looking to use.
Table Structures (now your objects probably have more data points than just a name
but for the sake of simplicity I am only going to keep track of a name
)
dbo.Device
CREATE TABLE dbo.Device
(
ID INT IDENTITY(1,1) NOT NULL,
Name VarChar(100) NOT NULL,
ParentDeviceID INT NULL,
ParentCombinerID INT NULL,
CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]
CREATE UNIQUE NONCLUSTERED INDEX IX_UniqueParentDeviceIDDevice
ON dbo.Device(ParentDeviceID)
WHERE ParentDeviceID IS NOT NULL
ALTER TABLE [dbo].[Device] WITH CHECK ADD CONSTRAINT [FK_Device_Device] FOREIGN KEY([ParentDeviceID])
REFERENCES [dbo].[Device] ([ID])
GO
ALTER TABLE [dbo].[Device] WITH CHECK ADD CONSTRAINT [FK_Combiner_Device] FOREIGN KEY([ParentCombinerID])
REFERENCES [dbo].[Combiner] ([ID])
GO
ALTER TABLE [dbo].[Device]
ADD CONSTRAINT CHK_Device CHECK (ParentDeviceID IS NULL OR ParentCombinerID IS NULL)
GO
dbo.Combiner
CREATE TABLE dbo.Combiner
(
ID INT IDENTITY(1,1) NOT NULL,
Name VarChar(100) NOT NULL,
ParentDeviceID INT NULL,
ParentCombinerID INT NULL,
CONSTRAINT [PK_Combiner] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]
CREATE UNIQUE NONCLUSTERED INDEX IX_UniqueParentDeviceIDCombiner
ON dbo.Combiner(ParentDeviceID)
WHERE ParentDeviceID IS NOT NULL
ALTER TABLE [dbo].[Combiner] WITH CHECK ADD CONSTRAINT [FK_Combiner_Device] FOREIGN KEY([ParentDeviceID])
REFERENCES [dbo].[Device] ([ID])
GO
ALTER TABLE [dbo].[Combiner] WITH CHECK ADD CONSTRAINT [FK_Combiner_Combiner] FOREIGN KEY([ParentCombinerID])
REFERENCES [dbo].[Combiner] ([ID])
GO
ALTER TABLE [dbo].[Combiner]
ADD CONSTRAINT CHK_Combiner CHECK (ParentDeviceID IS NULL OR ParentCombinerID IS NULL)
GO
Each table holds who its parent object is via ParentDeviceID
or ParentCombinerID
. The Unique Filtered Index
on ParentDeviceID
helps ensure that a given Device
can only be the parent of one object (A Unique Constraint
would stop multiple rows with a NULL
value). It isn't guaranteed so that is why we are going to use some triggers down the road. The two CHECK Constraint
s help ensure that any given object can only have 1 parent object instead of having both a Device
and Combiner
as a parent. The Foreign Key
s ensure that the values stored in ParentDeviceID
and ParentCombinerID
are valid values for those two tables.
Trigger
s
We can insert a Trigger
on each table
. These Trigger
s need to ensure:
- On the
insert
/update
of a dbo.Device
record a value stored on ParentDeviceID
is not already on dbo.Combiner.ParentDeviceID
- On the
Insert
/Update
of a dbo.Combiner
record a value stored on ParentDeviceID
is not already on dbo.Device.ParentDeviceID
-- On the Insert/Update of a dbo.Combiner record a value stored on ParentDeviceID is not already on dbo.Device.ParentDeviceID
CREATE TRIGGER dbo.CombinerCheck ON dbo.Combiner
AFTER INSERT, UPDATE
AS
BEGIN
IF EXISTS
(
SELECT *
FROM INSERTED I
INNER JOIN Device D
ON I.ParentDeviceID = D.ParentDeviceID
)
BEGIN
RAISERROR ('This Combiner cannot be added since it''s parent Device is already a parent to a different Device.', 16, 1);
ROLLBACK TRANSACTION; --stops the Insert/Update
RETURN
END
END
--On the insert/update of a dbo.Device record a value stored on ParentDeviceID is not already on dbo.Combiner.ParentDeviceID
CREATE TRIGGER dbo.DeviceCheck ON dbo.Device
AFTER INSERT, UPDATE
AS
BEGIN
IF EXISTS
(
SELECT *
FROM INSERTED I
INNER JOIN Combiner C
ON I.ParentDeviceID = C.ParentDeviceID
)
BEGIN
RAISERROR ('This Device cannot be added since it''s parent Device is already a parent to a Combiner.', 16, 1);
ROLLBACK TRANSACTION; --stops the Insert/Update
RETURN
END
END
As an alternative this StackOverflow question apperently give the same or similar effect with a Unique Index
on a View
. If you want to avoid Triggers
than this may be a solution for you. https://stackoverflow.com/questions/16314372/ms-sql-server-cross-table-constraint. I am not familiar with this approach but maybe it is a better solution for you.
Examples
Implementing the 1st Example provided (just 3 devices in a chain) would look something like this. Just 3 records in dbo.Device
:
+----+------+----------------+------------------+
| ID | Name | ParentDeviceID | ParentCombinerID |
+----+------+----------------+------------------+
| 1 | D1 | NULL | NULL |
| 2 | D2 | 1 | NULL |
| 3 | D3 | 2 | NULL |
+----+------+----------------+------------------+
Implementing the 2nd Example provided (1 Device --> 1 Combiner --> 3 Devices) would look something like this. 4 Records in dbo.Device
and 1 Record in dbo.Combiner
.
Device
+----+------+----------------+------------------+
| ID | Name | ParentDeviceID | ParentCombinerID |
+----+------+----------------+------------------+
| 1 | D1 | NULL | NULL |
| 2 | D2 | NULL | 1 |
| 3 | D3 | NULL | 1 |
| 4 | D4 | NULL | 1 |
+----+------+----------------+------------------+
Combiner
+----+------+----------------+------------------+
| ID | Name | ParentDeviceID | ParentCombinerID |
+----+------+----------------+------------------+
| 1 | C1 | 1 | NULL |
+----+------+----------------+------------------+
One more, slightly more complicated example to display all the relationship options:

dbo.Device
+----+------+----------------+------------------+
| ID | Name | ParentDeviceID | ParentCombinerID |
+----+------+----------------+------------------+
| 1 | D1 | NULL | NULL |
| 2 | D2 | 1 | NULL |
| 3 | D3 | NULL | 1 |
| 4 | D4 | NULL | 1 |
| 5 | D5 | NULL | 1 |
| 6 | D6 | NULL | 2 |
| 7 | D7 | NULL | 2 |
| 8 | D8 | NULL | 2 |
+----+------+----------------+------------------+
dbo.Combiner
+----+------+----------------+------------------+
| ID | Name | ParentDeviceID | ParentCombinerID |
+----+------+----------------+------------------+
| 1 | C1 | 2 | NULL |
| 2 | C2 | NULL | 1 |
+----+------+----------------+------------------+
Hopefully this give you all you need from the database side. Again I think this will be complicated on the App side navigating the table structures. But the database should keep the relationships the way you want to.
UNIQUE Constraint
that needs to be swapped out for aUNIQUE INDEX
. Just wanted to make sure you knew that piece was wrong in case it had not been handled on your own already.