]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
Add interim recipe for multi-tenant
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 29 Sep 2020 16:55:41 +0000 (12:55 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 29 Sep 2020 16:57:25 +0000 (12:57 -0400)
Alembic does not have a multi-tenant story right now.
for the schema use case, search_path represents the best
way to make it happen at a rudimental level.  Document
the basic idea for this as it is known to work for the moment.

Change-Id: I14f8eebc285f67bc374eb829e5fce49dc5820c9c
References: #555

docs/build/cookbook.rst

index 324c580c12ebae1bbeb4c1b2174b5fcb2779150c..08306a846a43bd79244a2211a45d44a3f0211224 100644 (file)
@@ -782,6 +782,114 @@ recreated again within the downgrade for this migration::
     INFO  [sqlalchemy.engine.base.Engine] {}
     INFO  [sqlalchemy.engine.base.Engine] COMMIT
 
+.. _cookbook_postgresql_multi_tenancy:
+
+Rudimental Schema-Level Multi Tenancy for PostgreSQL Databases
+==============================================================
+
+**Multi tenancy** refers to an application that accommodates for many
+clients simultaneously.   Within the scope of a database migrations tool,
+multi-tenancy typically refers to the practice of maintaining multiple,
+identical databases where each database is assigned to one client.
+
+Alembic does not currently have explicit multi-tenant support; typically,
+the approach must involve running Alembic multiple times against different
+database URLs.
+
+One common approach to multi-tenancy, particularly on the PostgreSQL database,
+is to install tenants within **individual PostgreSQL schemas**.  When using
+PostgreSQL's schemas, a special variable ``search_path`` is offered that is
+intended to assist with targeting of different schemas.
+
+.. note::  SQLAlchemy includes a system of directing a common set of
+   ``Table`` metadata to many schemas called `schema_translate_map <https://docs.sqlalchemy.org/core/connections.html#translation-of-schema-names>`_.   Alembic at the time
+   of this writing lacks adequate support for this feature.  The recipe below
+   should be considered **interim** until Alembic has more first-class support
+   for schema-level multi-tenancy.
+
+The recipe below can be altered for flexibility.  The primary purpose of this
+recipe is to illustrate how to point the Alembic process towards one PostgreSQL
+schema or another.
+
+1. The model metadata used as the target for autogenerate must not include any
+   schema name for tables; the schema must be non-present or set to ``None``.
+   Otherwise, Alembic autogenerate will still attempt
+   to compare and render tables in terms of this schema::
+
+
+        class A(Base):
+            __tablename__ = 'a'
+
+            id = Column(Integer, primary_key=True)
+            data = Column(UnicodeText())
+            foo = Column(Integer)
+
+            __table_args__ = {
+                "schema": None
+            }
+   ..
+
+2. The :paramref:`.EnvironmentContext.configure.include_schemas` flag must
+   also be False or not included.
+
+3. The "tenant" will be a schema name passed to Alembic using the "-x" flag.
+   In ``env.py`` an approach like the following allows ``-xtenant=some_schema``
+   to be supported by making use of :meth:`.EnvironmentContext.get_x_argument`::
+
+        def run_migrations_online():
+            connectable = engine_from_config(
+                config.get_section(config.config_ini_section),
+                prefix="sqlalchemy.",
+                poolclass=pool.NullPool,
+            )
+
+            current_tenant = context.get_x_argument(as_dictionary=True).get("tenant")
+            with connectable.connect() as connection:
+
+                # set search path on the connection, which ensures that
+                # PostgreSQL will emit all CREATE / ALTER / DROP statements
+                # in terms of this schema by default
+                connection.execute("set search_path to %s" % current_tenant)
+
+                # make use of non-supported SQLAlchemy attribute to ensure
+                # the dialect reflects tables in terms of the current tenant name
+                connection.dialect.default_schema_name = current_tenant
+
+                context.configure(
+                    connection=connection,
+                    target_metadata=target_metadata,
+                )
+
+                with context.begin_transaction():
+                    context.run_migrations()
+
+   The current tenant is set using the PostgreSQL ``search_path`` variable on
+   the connection.  Note above we must employ a **non-supported SQLAlchemy
+   workaround** at the moment which is to hardcode the SQLAlchemy dialect's
+   default schema name to our target schema.
+
+   It is also important to note that the above changes **remain on the connection
+   permanently unless reversed explicitly**.  If the alembic application simply
+   exits above, there is no issue.  However if the application attempts to
+   continue using the above connection for other purposes, it may be necessary
+   to reset these variables back to the default, which for PostgreSQL is usually
+   the name "public" however may be different based on configuration.
+
+
+4. Alembic operations will now proceed in terms of whichever schema we pass
+   on the command line.   All logged SQL will show no schema, except for
+   reflection operations which will make use of the ``default_schema_name``
+   attribute::
+
+       []$ alembic -x tenant=some_schema revision -m "rev1" --autogenerate
+
+   ..
+
+5. Since all schemas are to be maintained in sync, autogenerate should be run
+   against only **one** schema, generating new Alembic migration files.
+   Autogenerate migratin operations are then run against **all** schemas.
+
+
 .. _cookbook_no_empty_migrations:
 
 Don't Generate Empty Migrations with Autogenerate