postgres

package
v6.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 12, 2024 License: Apache-2.0 Imports: 11 Imported by: 0

README

PostgreSQL driver

Go Reference

The PostgreSQL driver provides multitenancy support for PostgreSQL databases using the gorm ORM.

The package is a thin wrapper around the gorm.io/driver/postgres driver, enhancing it with multitenancy support.

Conventions

TableName

The driver uses the public schema for public models and the tenant-specific schema for tenant-specific models. All models must implement the gorm.Tabler interface.

Public Model

The table name for public models must be prefixed with public..

type Tenant struct {
    ID uint `gorm:"primaryKey"`
    // other fields...
}

func (Tenant) TableName() string {
    return "public.tenants"
}
Tenant Model

The table name for tenant-specific models must not contain any prefix.

type Book struct {
    ID uint `gorm:"primaryKey"`
    // other fields...
}

func (Book) TableName() string {
    return "books"
}

TenantTabler

All tenant-specific models must implement the TenantTabler interface, which classifies the model as a tenant-specific model. The TenantTabler interface is used to determine which models to migrate when calling MigratePublicSchema or CreateSchemaForTenant.

type Book struct {
    ID uint `gorm:"primaryKey"`
    // other fields...
}

func (Book) IsTenantTable() bool {
    return true
}
Model Registration
After DB Initialization

Call RegisterModels after initializing the database to register all models.

import (
    "gorm.io/gorm"
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
)

db, err := gorm.Open(postgres.New(postgres.Config{
        DSN: "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable",
    }), &gorm.Config{})
if err != nil {
    panic(err)
}
err := postgres.RegisterModels(db, &Tenant{}, &Book{})
During DB Initialization

Alternatively, you can pass the models as variadic arguments to postgres.New when creating the dialect.

import (
    "gorm.io/gorm"
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
)

db, err := gorm.Open(postgres.New(postgres.Config{
        DSN: "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable",
    },&Tenant{}, &Book{}), &gorm.Config{})
if err != nil {
    panic(err)
}

Or pass the models as variadic arguments to postgres.Open when creating the dialect.

import (
    "gorm.io/gorm"
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
)

db, err := gorm.Open(postgres.Open(dsn, &Tenant{}, &Book{}), &gorm.Config{})
if err != nil {
    panic(err)
}
Migrations

After all models have been registered, we can perform table migrations.

Public Tables

Call MigratePublicSchema to create the public schema and migrate all public models.

import (
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
)

err := postgres.MigratePublicSchema(db)
Tenant Tables

Call CreateSchemaForTenant to create the schema for a tenant and migrate all tenant-specific models.

import (
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
)

err := postgres.CreateSchemaForTenant(db, tenantSchemaName)
Dropping Tenant Schemas

Call DropSchemaForTenant to drop the schema and cascade all schema tables.

import (
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
)

err := postgres.DropSchemaForTenant(db, tenantSchemaName)
Foreign Key Constraints

Conforming to the above conventions, foreign key constraints between public and tenant-specific models can be created just as if you were using a shared database and schema.

You can embed the postgres.TenantModel struct in your tenant model to add the necessary fields for the tenant model.

Then create a foreign key constraint between the public and tenant-specific models using the SchemaName field as the foreign key.

import (
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
    "gorm.io/gorm"
)

type Tenant struct {
    gorm.Model
    postgres.TenantModel
}

func (Tenant) TableName() string {
    return "public.tenants"
}

type Book struct {
    gorm.Model
    TenantSchema string `gorm:"column:tenant_schema"`
    Tenant       Tenant `gorm:"foreignKey:TenantSchema;references:SchemaName"`
}

func (Book) IsTenantTable() bool {
    return true
}

func (Book) TableName() string {
    return "books"
}
Tenant Schema Scopes
WithTenantSchema

Use the WithTenantSchema scope function when you want to perform operations on a tenant specific table, which may include foreign key constraints to a public schema table(s).

db.Scopes(WithTenantSchema(tenantID)).Find(&Book{})
SetSearchPath

Use the SetSearchPath function when the tenant schema table has foreign key constraints you want to access belonging to other tables in the same tenant schema (and or foreign key relations to public tables).

import (
    pgschema "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres/schema"
    "gorm.io/gorm"
)
db, resetSearchPath := pgschema.SetSearchPath(db, tenantSchemaName)
if err := db.Error(); err != nil {
    // handle error
}
defer resetSearchPath()
// No need to use any tenant scopes as the search path has been changed to the tenant's schema
db.Find(&Book{})
Benchmarks
  • goos: linux
  • goarch: amd64
  • pkg: github.com/bartventer/gorm-multitenancy/v6/drivers/postgres/schema
  • cpu: AMD EPYC 7763 64-Core Processor
  • environment: VSCode Codespaces 16GB RAM
  • date: 2024-05-12
Benchmark ns/op B/op allocs/op
BenchmarkScopingQueries/Create/WithTenantSchema-4 7587430 16086 208
BenchmarkScopingQueries/Create/SetSearchPath-4 292105 1672 25
BenchmarkScopingQueries/Find/WithTenantSchema-4 269289 4917 86
BenchmarkScopingQueries/Find/SetSearchPath-4 539173 6374 102
BenchmarkScopingQueries/Update/WithTenantSchema-4 7534239 13343 203
BenchmarkScopingQueries/Update/SetSearchPath-4 516340 6392 104
BenchmarkScopingQueries/Delete/WithTenantSchema-4 7630597 10848 181
BenchmarkScopingQueries/Delete/SetSearchPath-4 8168061 12088 189

Basic Example

Here's a simplified example of how to use the gorm-multitenancy package with the PostgreSQL driver:

package main

import (
    "gorm.io/gorm"
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
    "github.com/bartventer/gorm-multitenancy/v6/drivers/postgres/scopes"
)

// Tenant is a public model
type Tenant struct {
    gorm.Model
    postgres.TenantModel // Embed the TenantModel
}

// Implement the gorm.Tabler interface
func (t *Tenant) TableName() string {return "public.tenants"} // Note the public. prefix

// Book is a tenant specific model
type Book struct {
    gorm.Model
    Title        string
    TenantSchema string `gorm:"column:tenant_schema"`
    Tenant       Tenant `gorm:"foreignKey:TenantSchema;references:SchemaName"`
}

// Implement the gorm.Tabler interface
func (b *Book) TableName() string {return "books"} // Note the lack of prefix

// Implement the TenantTabler interface
func (b *Book) IsTenantTable() bool {return true} // This classifies the model as a tenant specific model

func main(){
    // Open a connection to the database
    db, err := gorm.Open(postgres.New(postgres.Config{
        DSN: "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable",
    }), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    // Register models
    if err := postgres.RegisterModels(db, &Tenant{}, &Book{}); err != nil {
        panic(err)
    }

    // Migrate the public schema
    if err := postgres.MigratePublicSchema(db); err != nil {
        panic(err)
    }

    // Create a tenant
    tenant := &Tenant{
        TenantModel: postgres.TenantModel{
            DomainURL: "tenant1.example.com",
            SchemaName: "tenant1",
        },
    }
    if err := db.Create(tenant).Error; err != nil {
        panic(err)
    }

    // Create the schema for the tenant
    if err := postgres.CreateSchemaForTenant(db, tenant.SchemaName); err != nil {
        panic(err)
    }

    // Create a book for the tenant
    b := &Book{
        Title: "Book 1",
        TenantSchema: tenant.SchemaName,
    }
    if err := db.Scopes(scopes.WithTenantSchema(tenant.SchemaName)).Create(b).Error; err != nil {
        panic(err)
    }

    // Drop the schema for the tenant
    if err := postgres.DropSchemaForTenant(db, tenant.SchemaName); err != nil {
        panic(err)
    }
}

Complete Examples

For more detailed examples, including how to use the middleware with different frameworks, please refer to the following:

Documentation

Overview

Package postgres provides a PostgreSQL driver for GORM, offering tools to facilitate the construction and management of multi-tenant applications.

Example:

package main

import (
	"gorm.io/gorm"
	"github.com/bartventer/gorm-multitenancy/v6/drivers/postgres"
	"github.com/bartventer/gorm-multitenancy/v6/drivers/postgres/scopes"
)

// Tenant is a public model
type Tenant struct {
    gorm.Model
    postgres.TenantModel // Embed the TenantModel
}

// Implement the gorm.Tabler interface
func (t *Tenant) TableName() string {return "public.tenants"} // Note the public. prefix

// Book is a tenant specific model
type Book struct {
    gorm.Model
    Title        string
    TenantSchema string `gorm:"column:tenant_schema"`
    Tenant       Tenant `gorm:"foreignKey:TenantSchema;references:SchemaName"`
}

// Implement the gorm.Tabler interface
func (b *Book) TableName() string {return "books"} // Note the lack of prefix

// Implement the TenantTabler interface
func (b *Book) IsTenantTable() bool {return true} // This classifies the model as a tenant specific model

func main(){
	// Open a connection to the database
    db, err := gorm.Open(postgres.New(postgres.Config{
        DSN: "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable",
    }), &gorm.Config{})
    if err != nil {
        panic(err)
    }

	// Register models
    if err := postgres.RegisterModels(db, &Tenant{}, &Book{}); err != nil {
        panic(err)
    }

	// Migrate the public schema
    if err := postgres.MigratePublicSchema(db); err != nil {
        panic(err)
    }

	// Create a tenant
    tenant := &Tenant{
        TenantModel: postgres.TenantModel{
            DomainURL: "tenant1.example.com",
            SchemaName: "tenant1",
        },
    }
    if err := db.Create(tenant).Error; err != nil {
        panic(err)
    }

	// Create the schema for the tenant
    if err := postgres.CreateSchemaForTenant(db, tenant.SchemaName); err != nil {
        panic(err)
    }

	// Create a book for the tenant
	b := &Book{
		Title: "Book 1",
		TenantSchema: tenant.SchemaName,
	}
	if err := db.Scopes(scopes.WithTenantSchema(tenant.SchemaName)).Create(b).Error; err != nil {
		panic(err)
	}

	// Drop the schema for the tenant
    if err := postgres.DropSchemaForTenant(db, tenant.SchemaName); err != nil {
        panic(err)
    }
}

Index

Constants

View Source
const (
	// PublicSchemaName is the name of the public schema.
	PublicSchemaName = "public"
)

Variables

This section is empty.

Functions

func CreateSchemaForTenant

func CreateSchemaForTenant(db *gorm.DB, schemaName string) error

CreateSchemaForTenant creates a new schema for a specific tenant in the PostgreSQL database, and migrates the private tables for the tenant. It takes a gorm.DB instance and the name of the schema as parameters. Returns an error if the schema creation fails.

func DropSchemaForTenant

func DropSchemaForTenant(db *gorm.DB, schemaName string) error

DropSchemaForTenant drops the schema for a specific tenant in the PostgreSQL database (CASCADING all objects in the schema). It takes a *gorm.DB instance and the name of the schema as parameters. Returns an error if there was a problem dropping the schema.

func MigratePublicSchema

func MigratePublicSchema(db *gorm.DB) error

MigratePublicSchema migrates the public schema in the database. It takes a *gorm.DB as input and returns an error if any.

func New

func New(config Config, models ...interface{}) gorm.Dialector

New creates a new PostgreSQL dialector with multitenancy support. It takes a Config struct as the first parameter and variadic models as the second parameter. The Config struct contains the necessary configuration for connecting to the PostgreSQL database. The models parameter is a list of GORM models that will be used for multitenancy configuration. It returns a gorm.Dialector that can be used with GORM. If there is an error during the creation of the multitenancy configuration, it will panic.

func Open

func Open(dsn string, models ...interface{}) gorm.Dialector

Open opens a connection to a PostgreSQL database using the provided DSN (Data Source Name) and models. It returns a gorm.Dialector that can be used to interact with the database. The models parameter is optional and can be used to specify the database models that should be registered. If an error occurs while creating the multitenancy configuration, it panics.

func RegisterModels

func RegisterModels(db *gorm.DB, models ...interface{}) error

RegisterModels registers the given models with the provided gorm.DB instance for multitenancy support. It initializes the multitenancy configuration for the database dialector. The models parameter should be a variadic list of model structs. Returns an error if there is a failure in registering the models or initializing the multitenancy configuration.

Types

type Config

type Config = postgres.Config

Config is the configuration for the postgres driver.

type Dialector

type Dialector struct {
	postgres.Dialector
	// contains filtered or unexported fields
}

Dialector is the postgres dialector with multitenancy support.

func (Dialector) Migrator

func (dialector Dialector) Migrator(db *gorm.DB) gorm.Migrator

Migrator returns a gorm.Migrator implementation for the Dialector. It creates a new instance of Migrator with the provided database connection and dialector. It also includes a multitenancyConfig that contains information about public models, tenant models, and all models. The Migrator is thread-safe and uses a sync.RWMutex for synchronization.

type Migrator

type Migrator struct {
	postgres.Migrator // gorm postgres migrator
	// contains filtered or unexported fields
}

Migrator is the struct that implements the MultitenancyMigrator interface.

func (Migrator) AutoMigrate

func (m Migrator) AutoMigrate(values ...interface{}) error

AutoMigrate migrates the specified values to the database. It checks for migration options in the context and performs the migration accordingly. If no migration options are found or if the migration options are invalid, an error is returned. The supported migration options are migrationOptionPublicTables and migrationOptionTenantTables. For any other migration option, an error is returned.

func (*Migrator) CreateSchemaForTenant

func (m *Migrator) CreateSchemaForTenant(tenant string) error

CreateSchemaForTenant creates a schema for a specific tenant in the database. It first checks if the schema already exists, and if not, it creates the schema. Then, it sets the search path to the newly created schema. After that, it migrates the private tables for the specified tenant. If there are no private tables to migrate, it returns an error. The function returns an error if any of the steps fail.

func (*Migrator) DropSchemaForTenant

func (m *Migrator) DropSchemaForTenant(tenant string) error

DropSchemaForTenant drops the schema for a specific tenant. It executes a transaction and drops the schema using the provided tenant name. If an error occurs during the transaction or while dropping the schema, it returns an error. Otherwise, it returns nil.

func (*Migrator) MigratePublicSchema

func (m *Migrator) MigratePublicSchema() error

MigratePublicSchema migrates the public tables in the database. It checks if there are any public tables to migrate and then performs the migration. If an error occurs during the migration, it logs the error. This function returns an error if there are no public tables to migrate or if an error occurs during the migration.

type MultitenancyMigrator

type MultitenancyMigrator interface {
	multitenancy.Migrator

	// CreateSchemaForTenant creates the schema for the tenant, and migrates the private tables
	//
	// Parameters:
	// 	- tenant: the tenant's schema name
	//
	// Returns:
	// 	- error: the error if any
	CreateSchemaForTenant(tenant string) error
	// DropSchemaForTenant drops the schema for the tenant (CASCADING tables)
	//
	// Parameters:
	// 	- tenant: the tenant's schema name
	//
	// Returns:
	// 	- error: the error if any
	DropSchemaForTenant(tenant string) error
	// MigratePublicSchema migrates the public tables
	MigratePublicSchema() error
}

MultitenancyMigrator is the interface for the postgres migrator with multitenancy support.

type TenantModel

type TenantModel struct {
	// DomainURL is the domain URL of the tenant
	DomainURL string `json:"domainURL" gorm:"column:domain_url;uniqueIndex;size:128"`

	// SchemaName is the schema name of the tenant and the primary key of the model.
	//
	// Field-level permissions are restricted to read and create.
	//
	// The following constraints are applied:
	// 	- unique index
	// 	- size: 63
	//
	// Additionally, check constraints are applied to ensure that the schema name adheres to the following rules:
	// 	- It must start with an underscore or a letter.
	// 	- The rest of the string can contain underscores, letters, and numbers.
	// 	- It must be at least 3 characters long.
	// 	- It must not start with 'pg_', as this prefix is reserved for system schemas.
	//
	// Examples of valid schema names:
	// 	- "tenant1"
	// 	- "_tenant"
	// 	- "tenant_1"
	//
	// Examples of invalid schema names:
	// 	- "1tenant" (does not start with an underscore or a letter)
	// 	- "pg_tenant" (starts with 'pg_')
	// 	- "t" (less than 3 characters long)
	//
	SchemaName string `` /* 152-byte string literal not displayed */
}

TenantModel a basic GoLang struct which includes the following fields: DomainURL, SchemaName. It's intended to be embedded into any public postgresql model that needs to be scoped to a tenant. It may be embedded into your model or you may build your own model without it.

For example:

type Tenant struct {
  postgres.TenantModel
}

type TenantPKModel

type TenantPKModel struct {
	// DomainURL is the domain URL of the tenant
	DomainURL string `json:"domainURL" gorm:"column:domain_url;uniqueIndex;size:128"`

	// SchemaName is the schema name of the tenant and the primary key of the model.
	// For details on the constraints and rules for this field, see [TenantModel.SchemaName].
	SchemaName string `` /* 163-byte string literal not displayed */
}

TenantPKModel is identical to TenantModel but with SchemaName as a primary key field.

Directories

Path Synopsis
Package schema provides utilities for managing PostgreSQL schemas in a multi-tenant application.
Package schema provides utilities for managing PostgreSQL schemas in a multi-tenant application.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL