multitenancy

package module
v1.0.9 Latest Latest
Warning

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

Go to latest
Published: Dec 28, 2023 License: Apache-2.0 Imports: 3 Imported by: 0

README

gorm-multitenancy

Go Reference Go Report Card Coverage Status Build License

There are three common approaches to multitenancy in a database:

  • Shared database, shared schema
  • Shared database, separate schemas
  • Separate databases

This package implements the shared database, separate schemas approach. It uses the gorm ORM to manage the database and provides custom drivers to support multitenancy. It also provides HTTP middleware to retrieve the tenant from the request and set the tenant in context.

Database compatibility

Current supported databases are listed below. Pull requests for other drivers are welcome.

Router compatibility

Current supported routers are listed below. Pull requests for other routers are welcome.

Installation

go get -u github.com/bartventer/gorm-multitenancy

Usage

PostgreSQL driver

For a complete example refer to the examples section.


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

// For models that are tenant specific, ensure that TenantTabler is implemented
// This classifies the model as a tenant specific model when performing subsequent migrations

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

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

// Book is a tenant specific model
type Book struct {
    gorm.Model // Embed the gorm.Model
    Title string
}

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

// Implement the TenantTabler interface
func (b *Book) IsTenantTable() bool {return true}

func main(){
    // Create the database connection
    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 the models
    // Models are categorized as either public or tenant specific, which allow for simpler migrations
    postgres.RegisterModels(
        db,        // Database connection
        // Public models (does not implement TenantTabler or implements TenantTabler with IsTenantTable() returning false)
        &Tenant{},  
        // Tenant specific model (implements TenantTabler)
        &Book{},
        )

    // Migrate the database
    // Calling AutoMigrate won't work, you must either call MigratePublicSchema or CreateSchemaForTenant
    // MigratePublicSchema will create the public schema and migrate all public models
    // CreateSchemaForTenant will create the schema for the tenant and migrate all tenant specific models

    // Migrate the public schema (migrates all public models)
    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)
    }

    // Migrate the tenant schema
    // This will create the schema and migrate all tenant specific models
    if err := postgres.CreateSchemaForTenant(db, tenant); err != nil {
        panic(err)
    }

    // Operations on tenant specific schemas (e.g. CRUD operations on books)
    // Refer to Examples section for more details on how to use the middleware

    // Drop the tenant schema
    // This will drop the schema and all tables in the schema
    if err := postgres.DropSchemaForTenant(db, tenant); err != nil {
        panic(err)
    }

    // ... other operations
}
echo middleware

For a complete example refer to the PostgreSQL with echo example.

import (
    "github.com/labstack/echo/v4"
    multitenancymw "github.com/bartventer/gorm-multitenancy/middleware/echo"
    "github.com/bartventer/gorm-multitenancy/scopes"
    // ...
)

func main(){
    // ...
    e := echo.New()
    // ... other middleware
    // Add the multitenancy middleware
    e.Use(multitenancymw.WithTenant(multitenancymw.WithTenantConfig{
        DB: db,
        Skipper: func(r *http.Request) bool {
			return strings.HasPrefix(r.URL.Path, "/tenants") // skip tenant routes
		},
        TenantGetters: multitenancymw.DefaultTenantGetters,
    }))
    // ... other middleware

    // ... routes
    e.GET("/books", func(c echo.Context) error {
        // Get the tenant from context
        tenant, _ := multitenancymw.TenantFromContext(c)
        var books []Book
        // Query the tenant specific schema
        if err := db.Scopes(scopes.WithTenant(tenant)).Find(&books).Error; err != nil {
            return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
        }
        return c.JSON(http.StatusOK, books)
    })

    // ... rest of code
}

net/http middleware

For a complete example refer to the PostgreSQL with net/http example.

import (
    "encoding/json"
    "net/http"

    multitenancymw "github.com/bartventer/gorm-multitenancy/middleware/nethttp"
    "github.com/go-chi/chi/v5"
    "github.com/bartventer/gorm-multitenancy/scopes"
    // ...
)

func main(){
    // ...
    r := chi.NewRouter() // your router of choice
    // ... other middleware
    // Add the multitenancy middleware
    r.Use(multitenancymw.WithTenant(multitenancymw.WithTenantConfig{
        DB: db,
        Skipper: func(r *http.Request) bool {
            return strings.HasPrefix(r.URL.Path, "/tenants") // skip tenant routes
        },
        TenantGetters: multitenancymw.DefaultTenantGetters, 
    }))
    // ... other middleware

    // ... routes
    r.Get("/books", func(w http.ResponseWriter, r *http.Request) {
        // Get the tenant from context
        tenant, _ := multitenancymw.TenantFromContext(r.Context())
        var books []Book
        // Query the tenant specific schema
        if err := db.Scopes(scopes.WithTenant(tenant)).Find(&books).Error; err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        if err := json.NewEncoder(w).Encode(books); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    })

    // ... rest of code
}

Examples

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Contributing

All contributions are welcome! Open a pull request to request a feature or submit a bug report.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Migrator

type Migrator interface {
	// AutoMigrate
	AutoMigrate(dst ...interface{}) error

	// Database
	CurrentDatabase() string
	FullDataTypeOf(*schema.Field) clause.Expr

	// Tables
	CreateTable(dst ...interface{}) error
	DropTable(dst ...interface{}) error
	HasTable(dst interface{}) bool
	RenameTable(oldName, newName interface{}) error
	GetTables() (tableList []string, err error)

	// Columns
	AddColumn(dst interface{}, field string) error
	DropColumn(dst interface{}, field string) error
	AlterColumn(dst interface{}, field string) error
	MigrateColumn(dst interface{}, field *schema.Field, columnType gorm.ColumnType) error
	HasColumn(dst interface{}, field string) bool
	RenameColumn(dst interface{}, oldName, field string) error
	ColumnTypes(dst interface{}) ([]gorm.ColumnType, error)

	// Views
	CreateView(name string, option gorm.ViewOption) error
	DropView(name string) error

	// Constraints
	CreateConstraint(dst interface{}, name string) error
	DropConstraint(dst interface{}, name string) error
	HasConstraint(dst interface{}, name string) bool

	// Indexes
	CreateIndex(dst interface{}, name string) error
	DropIndex(dst interface{}, name string) error
	HasIndex(dst interface{}, name string) bool
	RenameIndex(dst interface{}, oldName, newName string) error
}

Migrator is the interface for the migrator (as defined in gorm: https://gorm.io/docs/migration.html#Migrator-Interface)

type TenantTabler

type TenantTabler interface {
	// IsTenantTable returns true if the table is a tenant table
	IsTenantTable() bool
}

TenantTabler is the interface for tenant tables

Directories

Path Synopsis
drivers

Jump to

Keyboard shortcuts

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