Home | About | Resources | Contact | Partners
MUST Logo

Layout for printing

Download Free MUST Trial Today



"MUST allows our business to use Access to flexibly RAD our systems and then easily move production systems to SQL Server"
Karen Spencer, Partner and Business Manager,
Gravitas IT solutions

"FANTASTIC! What was going to take me hours hand (re)coding Stored Procs just worked..."
David Peach, I.T. Management & Support Services,
Qbt Consulting

"It has saved me a lot of time"
Julian Baker,
The Access Man



Multi-tenant Database Design
(21st April 2012)

In this article we will discuss one approach to creating a single database which could be used by multiple businesses (tenants). The most important design consideration is how to safely isolate the data between tenants, so that no tenant can see or influence in any way another tenant’s data.

One advantage of a multi-tenant system is that maintenance is reduced by having to maintain only one database which could be used by a large number of tenants.

Using Schemas to Partition the Design

In a multi-tenanted database design isolating each tenant’s data is going to be very important, we will do that by creating two sets of Schemas. Rather than creating tables on the dbo Schema (normal default Schema), SQL Server allows you to create a named Schema to hold tables and other design objects. Schemas let you partition the design, and are also extremely useful when managing security.

The first set of Schemas will all have names ending in _B suffix, for example: - Companies_B, Sales_B etc. These are the base schemas, and no user will have any permission on these Schemas.



The second set of Schemas will be named without the _B suffix, for example Companies, Sales etc. Users will have permissions on these Schemas and on each Schema we will create a View through which a user will interact with their data.



We will also create a synonym for each of the above views; this will enable any other views we have in the system to refer to the views without qualifying them with either the schema name or the ending _VW.



System Tables

We need to have a table which contains a list of the tenant businesses allowed to use the system. Below you can see an example of this table which would contain 1 row for each tenant. The field called CoCode holds the unique identifier for a tenant (Company Code).



For each user of the system we need to record which businesses data the user is entitled to see, this is recorded in a table which we call the parameter table (the actual name is tblParameters_M which was named so that it would be unlikely to conflict with the names of any application tables).

The ActiveCoCode fieldshown below indicates that the user works with tenant 1.



Filtering Data for each Tenant

We use the SUSER_SNAME() SQL Server function to get a user’s identity, and we have added additional string manipulations to remove the domain name.

SUBSTRING(SUSER_SNAME(),(CHARINDEX('\',SUSER_SNAME()) + 1),LEN(SUSER_SNAME()))

We then create a view which will only show the active users record from the parameter table.

CREATE VIEW [Reporting].[vw_tblParameters]
AS
SELECT * FROM [Reporting_B].[tblParameters_M]
WHERE [Parameters_UsernameFilter] =
   SUBSTRING(SUSER_SNAME(),(CHARINDEX('\',SUSER_SNAME()) + 1),LEN(SUSER_SNAME()))
WITH CHECK OPTION
GO

This view and all the other bottom level views that users works with have a WITH CHECK OPTION, this will prevent any user attempting by accident to insert data which cannot be reflected back through the view . This limits them to working only with their own businesses data.

The views through which the user will interact with the system, all have a similar structure:-

CREATE VIEW [Companies].[Shippers_VW]
AS
SELECT * FROM [Companies_B].[Shippers]
WHERE [CoCode] = (SELECT [ActiveCoCode] FROM [vw_tblParameters])
WITH CHECK OPTION
GO

Note: you would also want to take steps to ensure that a user is not able to change the value of their ActiveCoCode.

Base Table Structures

Each table in the system has a composite primary key, where the CoCode (unique tenant id) is part of the key.



ALTER TABLE [Sales_B].[Order Details] WITH CHECK ADD
CONSTRAINT [rel_OrdersOrderDetails] FOREIGN KEY([CoCode], [OrderID])
REFERENCES [Sales_B].[Orders] ([CoCode], [OrderID])
ON DELETE CASCADE
GO

Automatic Assignment of Tenant Identifier

When the application or a user inserts data into a table, they would need to know the value to insert for the CoCode field. To avoid the need for this we can design a function to get the CoCode for the user.

CREATE FUNCTION [dbo].[GetCompanyCode]()
RETURNS INT
AS
BEGIN
   Return (SELECT ActiveCoCode FROM Reporting.vw_tblParameters)
END
GO

We then add a default for the CoCode to all the base tables in the system, which uses the above function GetCompanyCode().

ALTER TABLE [Companies_B].[Shippers]
ADD
CONSTRAINT [def_Shippers_CoCode]
DEFAULT ([dbo].[GetCompanyCode]())
FOR [CoCode]
GO

Identity Columns and Multi-Tenanted Systems

Often you have a column with the Identity property set as part of the primary key on a table. Depending on whether these numbers are made visible by the application, you may want to consider the following potential issue.

Tenant 1 enters a sales order and sees a unique number 5006, a few days later they enter another sales order and see the next sequential unique number 6026. Because the automatic numbering is shared by all your tenants any given tenant they will not necessarily get a sequential series of numbers.

The easiest solution to this problem is to create your own table for holding unique sequenced numbers for each tenant, and then write some trigger code on each table to issue the sequential numbers for each tenant. You could display this new field in the application, instead of the existing field.

Note: Unfortunately we cannot use a function assigned as a default to achieve this.

CREATE TABLE [Admin_B].[tblRecordIDS](
[ID] [int]      IDENTITY(1,1) NOT NULL,
[CoCode]      INT NOT NULL,
[TableName]      VARCHAR(256) NULL,
[LastUsedId]      INT NULL,
[CommonName]      VARCHAR(255) NULL,
CONSTRAINT [pk_tblRecordIDS] PRIMARY KEY CLUSTERED
(
[CoCode] ASC,
[ID] ASC )
)
GO

INSERT INTO [Admin_B].[tblRecordIDS]
(CoCode,TableName,LastUsedId,CommonName)
VALUES(1,'Products',78,'Product Information')
GO

Then we add the new user interface key to the table, this has the same name as the existing field, but with a _UI suffix (User Interface).

ALTER TABLE [Products_B].[Products]
ADD ProductID_UI INT
GO



We now create a view and synonym for our table of unique identifiers.

CREATE VIEW [Admin].[tblRecordIDS_VW]
AS
SELECT * FROM [Admin_B].[tblRecordIDS]
WHERE [CoCode] = (SELECT [ActiveCoCode] FROM [vw_tblParameters])
GO

CREATE SYNONYM [dbo].[tblRecordIDS] FOR [Admin].[tblRecordIDS_VW]
GO


It is also useful to create a stored procedure which can be used to issue the new sequential numbers:-

CREATE PROC [dbo].[sp_GetUniqueKey]( @Tablename VARCHAR(128),
            @UniqueId INT OUTPUT)
AS
BEGIN
    DECLARE @NextId INT
    DECLARE @CoCode INT
    -- The synonym RecordIDS uses the user specific view RecordIDS_vw
    -- If not unique key exists then generate the key
    IF NOT EXISTS(SELECT LastUsedID FROM tblRecordIDS WHERE TableName = @TableName)
    BEGIN
        SET @CoCode = dbo.GetCompanyCode()
        INSERT INTO tblRecordIDS(CoCode,LastUsedID,TableName)
        VALUES(@CoCode,0,@TableName)
    END
    BEGIN TRY
        BEGIN TRAN
            -- use a dummy update to lock the record
            UPDATE tblRecordIDS
                SET LastUsedId = LastUsedId WHERE TableName = @TableName
            -- get last value
            SELECT @NextId = LastUsedId FROM tblRecordIDS
                WHERE TableName = @TableName
            -- increment value
            SET @NextId = @NextId + 1
            -- save the value
            UPDATE tblRecordIDS SET LastUsedId = @NextId
                WHERE TableName = @TableName
            -- commit the transaction
        COMMIT TRAN
    END TRY
    BEGIN CATCH
        ROLLBACK TRAN
        SET @nextID = -1
    END CATCH
    -- return the id to calling program
    SET @UniqueId = @NextId
END
GO

We can then create a trigger on the Products table which will issue the sequential numbering.

CREATE TRIGGER [Products_B].[TR_Products_I]
ON [Products_B].[Products]
FOR INSERT
AS
BEGIN
    -- gets new unique ids for primary key from a table
    -- note we can not use a function for this
    -- use a cursor and stored procedure
    DECLARE @NextUniqueId INT
    DECLARE @PrimarykeyPart INT

    -- Define the cursor
    DECLARE InsertedData CURSOR FOR
        SELECT ProductID_UI
        FROM [Products_B].Products
        -- don’t use a join here
        WHERE [ProductID] = (SELECT [ProductID] FROM [INSERTED])
        FOR UPDATE OF ProductID_UI

    -- process all new records
    OPEN InsertedData
    FETCH NEXT
    FROM InsertedData INTO @PrimarykeyPart
    WHILE @@FETCH_STATUS = 0
    BEGIN
        -- Get next unique id
        EXEC sp_GetUniqueKey ‘Products’ ,@NextUniqueId OUTPUT
        -- update the key
        UPDATE [Products_B].[Products]
        SET ProductID_UI = @NextUniqueId
        WHERE CURRENT OF InsertedData

        FETCH NEXT
        FROM InsertedData INTO @PrimarykeyPart
    END
    CLOSE InsertedData
    DEALLOCATE InsertedData
END
GO

We can then test what happens when a new record is added to the table (after we refresh the product view so we can see the new UI field):-

sp_refreshview 'Products.Products_VW'
go

INSERT INTO Products(ProductName)
VALUES('Test Product')
GO

SELECT CoCode, ProductID, ProductName, ProductID_UI
FROM Products
WHERE Productname = 'Test Product'





Although ProductID is sequential at the moment after different tenants have performed inserts it will no longer be sequential, but ProductID_UI will remain sequential.

Assigning Security with Database Roles and Schemas

One of the easiest methods for implementing security is to construct a database role, then grant permissions to the role. Note that the role will only have permissions on the Schemas containing views, and not any of the Schemas containing tables.

CREATE ROLE GeneralUsers
GO

GRANT SELECT, INSERT,UPDATE,DELETE, EXECUTE
ON SCHEMA ::Companies TO GeneralUsers
GO



When new users are added to the database, they are assigned to this role.

Summary

In this article we have shown one approach to creating a multi-tenanted database design, subsequent articles will address techniques for developing .net applications which utilise this design.

MUST+SQL allows you to upsize into multiple schemas splitting up an application into manageable chunks or bringing together multiple databases and simplifying security. Click below to improve your development productivity.



Top of Page