DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify
  • How Java Apps Litter Beyond the Heap
  • Schema Change Management Tools: A Practical Overview
  • What Are SpeedUp and ScaleUp in DBMS?

Trending

  • Microsoft Azure Synapse Analytics: Scaling Hurdles and Limitations
  • Mastering Fluent Bit: Installing and Configuring Fluent Bit on Kubernetes (Part 3)
  • The Modern Data Stack Is Overrated — Here’s What Works
  • How AI Agents Are Transforming Enterprise Automation Architecture
  1. DZone
  2. Data Engineering
  3. Databases
  4. Shared Schema Strategy With Postgres (Part 1)

Shared Schema Strategy With Postgres (Part 1)

Apply the shared schema strategy in Postgres by using the row-level security policy.

By 
Szymon Tarnowski user avatar
Szymon Tarnowski
DZone Core CORE ·
Feb. 09, 21 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
12.1K Views

Join the DZone community and get the full member experience.

Join For Free

The shared schema strategy is one of the main multi-tenancy strategies besides the separate database or separate schema. Although there is no limitation, there has to be only one schema in the database, but all tenants should have the same access. The strategy assumes that all tables in a database (except tables that store vocabulary data or data available for all tenants) have a column that stores tenant identifiers. Based on this value column, we know which tenant is the owner of the table row.

Obviously, this approach offers a lower isolation level than the separate database and shared schema strategies but, just like the shared schema strategy, saves infrastructure costs. In comparison to the shared schema strategy, we can say that, although Postgres can handle multiple schemas, we can find comments from people who use this strategy with Postgres for a larger number of tenants. The maintenance for this approach, however, is harder than when the schema is shared between tenants. Executing DDL scripts for hundreds of schemas might be easily automated, but still, it might create complexity and slower deployment. It does not mean that executing DDL scripts for a database with a shared schema approach is error-free.

Implementation

The shared schema strategy assumes that all tables shared by the tenant have a column that defines its owner. This means that the value of this column has to be checked in each SQL query and operation. The column must be checked during the SELECT statement execution as well as all operations that modify data like UPDATE, INSERT and DELETE. 

One approach would be that the application should handle these checks. In every SQL statement sent to the database, the application must attach a condition that checks the tenant column's value. It might be easier if the application uses some ORM framework. But still, if the application code contains any custom query, the developer has to be aware that he needs to add checks for the tenant identifier column. The pros for this approach are that if we would ever consider changing the database engine for the application, it would be quick and easy. However, there are cons for such a situation. Say, for example, we had many applications that use the same database, then ll applications should have implemented tenant column checks; not to mention if those applications are written in different programming languages.

The Implementation That Uses a Database Engine

Another approach similar to the previous one, but from a different side, is when the database engine is doing tenant column value checks. Postgres has a feature called the row security policy that allows for the implementation of such a solution. Probably the biggest advantage of this approach is that it takes a huge amount of responsibility from the application code. Of course, the application needs to implement how to create connections per specific tenants. But, despite that, the programmer does not have to worry about the tenant column's missing condition in the SQL statement unless the SQL statement is so custom that the developer changes the tenant during its execution.

Implementation Tips

No matter if we decide to choose the application approach or database approach, it needs to be mentioned that our database constraints need to be wisely designed, especially those constraints that check if the record values are unique. Designing table primary keys that would contain tenant columns is also worth considering; at least for tables that store data of different tenants. Dictionary tables used by all tenants don't need to have such requirements.

Posmulten

Posmulten is an open-source project that generates DDL statements that create a shared schema strategy for your project that uses the Postgres database. Posmulten uses a row security policy mechanism to handle tenant data isolation. Below there's a script example with DDL statements that create a simple schema. In this script, all tables have a primary key, which part is the tenant column. There is also the possibility to generate a shared schema strategy for a model where the tenant column is not a part of the primary key. Such an approach might be easier to adopt to existed applications. This approach will be described in the next part of this article.

SQL
 




x


 
1
CREATE TABLE public.user_info
2
(
3
    id bigint NOT NULL,
4
    tenant character varying(255) COLLATE pg_catalog."default" NOT NULL,
5
    name character varying(32) COLLATE pg_catalog."default" NOT NULL,
6
    manager_id bigint,
7
    CONSTRAINT user_info_pkey PRIMARY KEY (id, tenant),
8
    CONSTRAINT manager_fk FOREIGN KEY (manager_id, tenant)
9
        REFERENCES public.user_info (id, tenant) MATCH SIMPLE
10
        ON UPDATE NO ACTION
11
        ON DELETE NO ACTION
12
)
13
WITH (
14
    OIDS = FALSE
15
)
16
TABLESPACE pg_default;
17
 
          
18
ALTER TABLE public.user_info
19
    OWNER to "postgresql-core-owner";
20
    
21
CREATE TABLE public.groups
22
(
23
    id bigint NOT NULL,
24
    tenant character varying(255) COLLATE pg_catalog."default" NOT NULL,
25
    name character varying(255) COLLATE pg_catalog."default" NOT NULL,
26
    CONSTRAINT groups_pkey PRIMARY KEY (id, tenant)
27
)
28
WITH (
29
    OIDS = FALSE
30
)
31
TABLESPACE pg_default;
32
 
          
33
ALTER TABLE public.groups
34
    OWNER to "postgresql-core-owner";
35
    
36
CREATE TABLE public.posts
37
(
38
    id bigint NOT NULL,
39
    tenant character varying(255) COLLATE pg_catalog."default" NOT NULL,
40
    name character varying(255) COLLATE pg_catalog."default" NOT NULL,
41
    user_id bigint,
42
    CONSTRAINT posts_pkey PRIMARY KEY (id, tenant),
43
    CONSTRAINT users_fk FOREIGN KEY (tenant, user_id)
44
        REFERENCES public.user_info (tenant, id) MATCH SIMPLE
45
        ON UPDATE NO ACTION
46
        ON DELETE NO ACTION
47
)
48
WITH (
49
    OIDS = FALSE
50
)
51
TABLESPACE pg_default;
52
 
          
53
ALTER TABLE public.posts
54
    OWNER to "postgresql-core-owner";
55
    
56
CREATE TABLE public.user_groups
57
(
58
    user_id bigint,
59
    group_id bigint,
60
    tenant character varying(255) COLLATE pg_catalog."default",
61
    CONSTRAINT group_fk FOREIGN KEY (group_id, tenant)
62
        REFERENCES public.groups (id, tenant) MATCH SIMPLE
63
        ON UPDATE NO ACTION
64
        ON DELETE NO ACTION,
65
    CONSTRAINT user_fk FOREIGN KEY (tenant, user_id)
66
        REFERENCES public.user_info (tenant, id) MATCH SIMPLE
67
        ON UPDATE NO ACTION
68
        ON DELETE NO ACTION
69
)
70
WITH (
71
    OIDS = FALSE
72
)
73
TABLESPACE pg_default;
74
 
          
75
ALTER TABLE public.user_groups
76
    OWNER to "postgresql-core-owner";


As Posmulten is a Java library, there is a wrapper script that us it, and based on the configuration file, it creates two files with DDL statements (create_script.sql and drop_script.sql).

The drop_script.sql file contains statements that drop all changes applied by the create_script.sql file. Be in mind that those instructions are not compensations operations. They do not restore state before applying the create_script.sql script. Below there is an example of a configuration file. Please check the site for project configuration-yaml-interpreter to know how to prepare a configuration file.

YAML
 




xxxxxxxxxx
1
23


 
1
default_schema: public
2
current_tenant_id_property_type:  "VARCHAR(255)"
3
default_tenant_id_column: "tenant"
4
grantee: "postgresql-core-user"
5
set_current_tenant_identifier_as_default_value_for_tenant_column_in_all_tables: true
6
valid_tenant_value_constraint:
7
  is_tenant_valid_function_name:  is_t_valid
8
  is_tenant_valid_constraint_name:  "is_tenant_valid_constraint_SDFA"
9
  tenant_identifiers_blacklist:
10
    - "DUMMY_TENANT"
11
tables:
12
  - name: user_info
13
    rls_policy:
14
      name: users_table_rls_policy
15
  - name: posts
16
    rls_policy:
17
      name: "posts_table_rls_policy"
18
  - name: groups
19
    rls_policy:
20
      name: groups_table_rls_policy
21
  - name: user_groups
22
    rls_policy:
23
      name: user_groups_table_rls_policy


After executing the wrapper script for our configuration file:

Shell
 




x


 
1
posmulten-ddl.sh configuration.yaml


We get the create_script.sql script just like below:

SQL
 




xxxxxxxxxx
1
59


 
1
CREATE OR REPLACE FUNCTION public.get_current_tenant_id() RETURNS VARCHAR(255) AS $$
2
SELECT current_setting('posmulten.tenant_id')
3
$$ LANGUAGE sql
4
STABLE
5
PARALLEL SAFE;
6
CREATE OR REPLACE FUNCTION public.set_current_tenant_id(VARCHAR(255)) RETURNS VOID AS $$
7
BEGIN
8
PERFORM set_config('posmulten.tenant_id', $1, false);
9
END
10
$$ LANGUAGE plpgsql
11
VOLATILE;
12
CREATE OR REPLACE FUNCTION public.is_id_equals_current_tenant_id(VARCHAR(255)) RETURNS BOOLEAN AS $$
13
SELECT $1 = public.get_current_tenant_id()
14
$$ LANGUAGE sql
15
STABLE
16
PARALLEL SAFE;
17
CREATE OR REPLACE FUNCTION public.tenant_has_authorities(VARCHAR(255), VARCHAR(255), VARCHAR(255), VARCHAR(255), VARCHAR(255)) RETURNS BOOLEAN AS $$
18
SELECT public.is_id_equals_current_tenant_id($1)
19
$$ LANGUAGE sql
20
STABLE
21
PARALLEL SAFE;
22
CREATE OR REPLACE FUNCTION public.is_t_valid(VARCHAR(255)) RETURNS BOOLEAN AS $$
23
SELECT $1 <> CAST ('DUMMY_TENANT' AS VARCHAR(255))
24
$$ LANGUAGE sql
25
IMMUTABLE
26
PARALLEL SAFE;
27
ALTER TABLE public."user_info" ENABLE ROW LEVEL SECURITY;
28
ALTER TABLE public."user_groups" ENABLE ROW LEVEL SECURITY;
29
ALTER TABLE public."posts" ENABLE ROW LEVEL SECURITY;
30
ALTER TABLE public."groups" ENABLE ROW LEVEL SECURITY;
31
CREATE POLICY users_table_rls_policy ON public.user_info
32
FOR ALL
33
TO "postgresql-core-user"
34
USING (public.tenant_has_authorities(tenant, 'ALL', 'USING', 'user_info', 'public'))
35
WITH CHECK (public.tenant_has_authorities(tenant, 'ALL', 'WITH_CHECK', 'user_info', 'public'));
36
CREATE POLICY user_groups_table_rls_policy ON public.user_groups
37
FOR ALL
38
TO "postgresql-core-user"
39
USING (public.tenant_has_authorities(tenant, 'ALL', 'USING', 'user_groups', 'public'))
40
WITH CHECK (public.tenant_has_authorities(tenant, 'ALL', 'WITH_CHECK', 'user_groups', 'public'));
41
CREATE POLICY posts_table_rls_policy ON public.posts
42
FOR ALL
43
TO "postgresql-core-user"
44
USING (public.tenant_has_authorities(tenant, 'ALL', 'USING', 'posts', 'public'))
45
WITH CHECK (public.tenant_has_authorities(tenant, 'ALL', 'WITH_CHECK', 'posts', 'public'));
46
CREATE POLICY groups_table_rls_policy ON public.groups
47
FOR ALL
48
TO "postgresql-core-user"
49
USING (public.tenant_has_authorities(tenant, 'ALL', 'USING', 'groups', 'public'))
50
WITH CHECK (public.tenant_has_authorities(tenant, 'ALL', 'WITH_CHECK', 'groups', 'public'));
51
ALTER TABLE "public"."user_info" ADD CONSTRAINT is_tenant_valid_constraint_SDFA CHECK (tenant IS NULL OR public.is_t_valid(tenant));
52
ALTER TABLE "public"."user_groups" ADD CONSTRAINT is_tenant_valid_constraint_SDFA CHECK (tenant IS NULL OR public.is_t_valid(tenant));
53
ALTER TABLE "public"."posts" ADD CONSTRAINT is_tenant_valid_constraint_SDFA CHECK (tenant IS NULL OR public.is_t_valid(tenant));
54
ALTER TABLE "public"."groups" ADD CONSTRAINT is_tenant_valid_constraint_SDFA CHECK (tenant IS NULL OR public.is_t_valid(tenant));
55
ALTER TABLE public.user_info ALTER COLUMN tenant SET DEFAULT public.get_current_tenant_id();
56
ALTER TABLE public.user_groups ALTER COLUMN tenant SET DEFAULT public.get_current_tenant_id();
57
ALTER TABLE public.posts ALTER COLUMN tenant SET DEFAULT public.get_current_tenant_id();
58
ALTER TABLE public.groups ALTER COLUMN tenant SET DEFAULT public.get_current_tenant_id();
59
 
          


When the superuser or table owner can apply changes, the security policy is designed for the "postgresql-core-user" user, which should not be a superuser. However, the user can be the table owner. Now let's try to insert some data. 

For this article's purpose, we assume that the "postgresql-core-user" user has privileges that allow modifying tables (INSERT, SELECT, UPDATE, DELETE) in the schema for which DDL statements were applied.

Insert Data

SQL
 




xxxxxxxxxx
1
13


 
1
SELECT set_current_tenant_id('XXX');
2
INSERT INTO user_info(id, tenant, name) VALUES (1, 'XXX', 'manager1');
3
INSERT INTO user_info(id, tenant, name, manager_id) VALUES (2, 'XXX', 'user1', 1);
4
INSERT INTO user_info(id, name) VALUES (3, 'user 2');
5
 
          
6
SELECT set_current_tenant_id('YYY');
7
INSERT INTO user_info(id, name) VALUES (1, 'manager1');
8
INSERT INTO user_info(id, name, manager_id) VALUES (2, 'user1', 1);
9
INSERT INTO user_info(id, name) VALUES (3, 'second user for YYY');
10
 
          
11
SELECT set_current_tenant_id('XXX');
12
INSERT INTO posts(id, tenant, name) VALUES (1, 'XXX', 'post 1');
13
INSERT INTO posts(id, tenant, name, user_id) VALUES (2, 'XXX', 'post 2', 1);


The above script is an example of how data should be inserted by the "postgresql-core-user" The first thing that catches our attention is calling the set_current_tenant_id(?) method. Btw, all names of methods generated by the posmulten library can be customized. The function sets a tenant identifier for the current connection session. You may notice that some INSERT instruction does not have specified the tenant column in the VALUES section. The posmulten library can generate the DDL statement that sets the default value defined for the tenant column.

Executing SQL statement without setting tenant:

Let's try to check what will happen when the tenant is not being set, and we are trying to add data.

SQL
 




xxxxxxxxxx
1


 
1
INSERT INTO user_info(id, tenant, name) VALUES (4, 'XXX', 'user4');


We can get such error:

PLSQL
 




xxxxxxxxxx
1


 
1
ERROR:  unrecognized configuration parameter "posmulten.tenant_id"
2
SQL state: 42704



Executing SQL statement with setting different tenant:

Let's try to check what will happen when we set a different tenant than an INSERT statement:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('YYY');
2
INSERT INTO user_info(id, tenant, name) VALUES (4, 'XXX', 'user4');


We can get such error:

PLSQL
 




xxxxxxxxxx
1


 
1
ERROR:  new row violates row-level security policy for table "user_info"
2
SQL state: 42501



Modify Data

Below there is an example of how to execute UPDATE instruction:

SQL
 




x


 
1
SELECT set_current_tenant_id('XXX');
2
UPDATE user_info SET name = 'main manager' WHERE id = 1


Output:

PLSQL
 




xxxxxxxxxx
1


 
1
UPDATE 1


Based on the output, we can see that one record was updated.

Executing UPDATE statement for data that belongs to the different tenant:

Below there is an example where we executed UPDATE instruction for a tenant that does not have any records. However, the rows with the specified value for id exists.

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('No_such_tenant');
2
UPDATE user_info SET name = 'Boss' WHERE id = 1;


Output:

PLSQL
 




xxxxxxxxxx
1


 
1
UPDATE 0


Based on the output, we can see that no record was updated.

Executing SQL statement without setting tenant:

Let's try to check what will happen when the tenant is not being set, and we are trying to modify data.

SQL
 




xxxxxxxxxx
1


 
1
UPDATE user_info SET name = 'Boss' WHERE id = 1;


We can get such error, the same which had with the similar case for INSERT statement:

PLSQL
 




xxxxxxxxxx
1


 
1
ERROR:  unrecognized configuration parameter "posmulten.tenant_id"
2
SQL state: 42704



Searching Data

Let's add some more data for the presentation:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
INSERT INTO posts(id, name, user_id) VALUES (11, 'post 11', 1);
3
INSERT INTO posts(id, name, user_id) VALUES (12, 'post 12', 1);
4
INSERT INTO posts(id, name, user_id) VALUES (13, 'post 13', 2);
5
 
          
6
SELECT set_current_tenant_id('YYY');
7
INSERT INTO posts(id, name, user_id) VALUES (21, 'post 21', 1);



Selecting data for "XXX" tenant:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
SELECT COUNT(*) FROM posts;


Output:

PLSQL
 




xxxxxxxxxx
1


 
1
"5"



SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
SELECT * FROM posts;


Output:

 Selecting data for "YYY" tenant:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('YYY');
2
SELECT COUNT(*) FROM posts;


Output:

PLSQL
 




xxxxxxxxxx
1


 
1
"1"



SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('YYY');
2
SELECT * FROM posts;


Output:

Search statement with JOIN instruction:

Selecting data for "XXX" tenant:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
SELECT u.id, u.tenant, u.name, p.* FROM user_info u JOIN posts p ON u.id = p.user_id;


Output:

Selecting data for "YYY" tenant:

SQL
 




x


 
1
SELECT set_current_tenant_id('YYY');
2
SELECT u.id, u.tenant, u.name, p.* FROM user_info u JOIN posts p ON u.id = p.user_id;


Output:

Search statement with LEFT JOIN instruction:

Selecting data for "XXX" tenant:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
SELECT u.id, u.tenant, u.name, p.* FROM user_info u LEFT JOIN posts p ON u.id = p.user_id;


Output:

Search statement with RIGHT JOIN instruction:

Selecting data for "XXX" tenant:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
SELECT u.id, u.tenant, u.name, p.* FROM user_info u RIGHT JOIN posts p ON u.id = p.user_id;


Output:

Selecting all data:

It is always possible to select all data by superuser or table owner (unless row level policy is forced for checking a table owner) without setting the current tenant.

SQL
 




xxxxxxxxxx
1


 
1
SELECT * FROM posts;


Output:

Deleting Data

The Row Level Security is applied for the DELETE statement. Below there is an example deleting all data for one tenant (without WHERE phrase) and then displaying data for the other tenant.  

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('XXX');
2
DELETE FROM posts;


Output:

PLSQL
 




xxxxxxxxxx
1


 
1
DELETE 5



Selecting data for the different tenant:

SQL
 




xxxxxxxxxx
1


 
1
SELECT set_current_tenant_id('YYY');
2
SELECT * FROM posts;


Output:

Summary

Based on the presented examples, we can admit that it is possible to implement the shared schema strategy in the Postgres database by using the row-level security policy.

Database engine sql Schema Data (computing) PostgreSQL application

Opinions expressed by DZone contributors are their own.

Related

  • How to Build a Full-Stack App With Next.js, Prisma, Postgres, and Fastify
  • How Java Apps Litter Beyond the Heap
  • Schema Change Management Tools: A Practical Overview
  • What Are SpeedUp and ScaleUp in DBMS?

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!