Skip to content

Commit

Permalink
docs(README): simplify example usage for maintainability
Browse files Browse the repository at this point in the history
`Echo` and `nett/http` example snippets have been removed as links are present with detailed usage for each.
  • Loading branch information
bartventer committed Apr 26, 2024
1 parent 5afea8d commit 635db60
Showing 1 changed file with 50 additions and 163 deletions.
213 changes: 50 additions & 163 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,31 @@
## Multitenancy Approaches

There are three common approaches to multitenancy in a database:
- Shared database, shared schema
- Shared database, separate schemas
- Separate databases

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

This package implements the shared database, separate schemas approach to multitenancy, providing custom drivers for seamless integration with your existing database setup.

## Features

- **GORM Integration**: Uses the [gorm](https://gorm.io/) ORM to manage the database, allowing for easy integration with your existing GORM setup.
- **Custom Database Drivers**: Provides custom drivers to support multitenancy, allowing you to easily swap and change with your existing drivers with minimal initialization reconfiguration.
- **HTTP Middleware**: Includes middleware for seamless integration with certain routers, enabling the retrieval of the tenant from the request and setting the tenant in context.
- **GORM Integration**: Uses the [gorm](https://gorm.io/) ORM to manage the database, allowing for easy integration with your existing GORM setup.
- **Custom Database Drivers**: Provides custom drivers to support multitenancy, allowing you to easily swap and change with your existing drivers with minimal initialization reconfiguration.
- **HTTP Middleware**: Includes middleware for seamless integration with certain routers, enabling the retrieval of the tenant from the request and setting the tenant in context.

## Database compatibility

Current supported databases are listed below. Pull requests for other drivers are welcome.
- [PostgreSQL](https://www.postgresql.org/)

- [PostgreSQL](https://www.postgresql.org/)

## Router Integration

This package includes middleware that can be utilized with the routers listed below for seamless integration with the database drivers. While not a requirement, these routers are fully compatible with the provided middleware. Contributions for other routers are welcome.
- [echo](https://echo.labstack.com/docs)
- [net/http](https://golang.org/pkg/net/http/)

- [echo](https://echo.labstack.com/docs)
- [net/http](https://golang.org/pkg/net/http/)

## Installation

Expand All @@ -51,29 +56,31 @@ go get -u github.com/bartventer/gorm-multitenancy/v5
### PostgreSQL driver

#### Conventions
- 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
- The table name for public models must be prefixed with `public.` (e.g. `public.books`), whereas the table name for tenant specific models must not contain any prefix (e.g. only `books`)
- All tenant specific models must implement the [TenantTabler](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/#TenantTabler) interface, which classifies the model as a tenant specific model:
- The `TenantTabler` interface has a single method `IsTenantTable() bool` which returns `true` if the model is tenant specific and `false` otherwise
- The `TenantTabler` interface is used to determine which models to migrate when calling `MigratePublicSchema` or `CreateSchemaForTenant`
- Models can be registered in two ways:
- When creating the dialect, by passing the models as variadic arguments to [`postgres.New`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#New) (e.g. `postgres.New(postgres.Config{...}, &Book{}, &Tenant{})`) or by calling [`postgres.Open`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#Open) (e.g. `postgres.Open("postgres://...", &Book{}, &Tenant{})`)
- By calling [`postgres.RegisterModels`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#RegisterModels) (e.g. `postgres.RegisterModels(db, &Book{}, &Tenant{})`)
- Migrations can be performed in two ways (after registering the models):
- By calling [`MigratePublicSchema`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#MigratePublicSchema) to create the public schema and migrate all public models
- By calling [`CreateSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#CreateSchemaForTenant) to create the schema for the tenant and migrate all tenant specific models
- To drop a tenant schema, call [`DropSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#DropSchemaForTenant); this will drop the schema and all tables in the schema
- When creating the dialect, by passing the models as variadic arguments to [`postgres.New`]((https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#New)) (e.g. `postgres.New(postgres.Config{...}, &Book{}, &Tenant{})`) or by calling [`postgres.Open`]((https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#Open)) (e.g. `postgres.Open("postgres://...", &Book{}, &Tenant{})`)
- By calling [`postgres.RegisterModels`]((https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#RegisterModels)) (e.g. `postgres.RegisterModels(db, &Book{}, &Tenant{})`)
- Migrations can be performed in two ways (after registering the models):
- By calling [`MigratePublicSchema`]((https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#MigratePublicSchema)) to create the public schema and migrate all public models
- By calling [`CreateSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#CreateSchemaForTenant) to create the schema for the tenant and migrate all tenant specific models
- To drop a tenant schema, call [`DropSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#DropSchemaForTenant); this will drop the schema and cascade all schema tables

- 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
- The table name for public models must be prefixed with `public.` (e.g. `public.books`), whereas the table name for tenant specific models must not contain any prefix (e.g. only `books`)
- All tenant specific models must implement the [TenantTabler](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/#TenantTabler) interface, which classifies the model as a tenant specific model:
- The `TenantTabler` interface has a single method `IsTenantTable() bool` which returns `true` if the model is tenant specific and `false` otherwise
- The `TenantTabler` interface is used to determine which models to migrate when calling `MigratePublicSchema` or `CreateSchemaForTenant`
- Models can be registered in two ways:
- When creating the dialect, by passing the models as variadic arguments to [`postgres.New`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#New) (e.g. `postgres.New(postgres.Config{...}, &Book{}, &Tenant{})`) or by calling [`postgres.Open`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#Open) (e.g. `postgres.Open("postgres://...", &Book{}, &Tenant{})`)
- By calling [`postgres.RegisterModels`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#RegisterModels) (e.g. `postgres.RegisterModels(db, &Book{}, &Tenant{})`)
- Migrations can be performed in two ways (after registering the models):
- By calling [`MigratePublicSchema`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#MigratePublicSchema) to create the public schema and migrate all public models
- By calling [`CreateSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#CreateSchemaForTenant) to create the schema for the tenant and migrate all tenant specific models
- To drop a tenant schema, call [`DropSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#DropSchemaForTenant); this will drop the schema and all tables in the schema
- When creating the dialect, by passing the models as variadic arguments to [`postgres.New`](<(https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#New)>) (e.g. `postgres.New(postgres.Config{...}, &Book{}, &Tenant{})`) or by calling [`postgres.Open`](<(https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#Open)>) (e.g. `postgres.Open("postgres://...", &Book{}, &Tenant{})`)
- By calling [`postgres.RegisterModels`](<(https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#RegisterModels)>) (e.g. `postgres.RegisterModels(db, &Book{}, &Tenant{})`)
- Migrations can be performed in two ways (after registering the models):
- By calling [`MigratePublicSchema`](<(https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#MigratePublicSchema)>) to create the public schema and migrate all public models
- By calling [`CreateSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#CreateSchemaForTenant) to create the schema for the tenant and migrate all tenant specific models
- To drop a tenant schema, call [`DropSchemaForTenant`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#DropSchemaForTenant); this will drop the schema and cascade all schema tables

#### Foreign Key Constraints
- Conforming to the [above conventions](#conventions), foreign key constraints between public and tenant specific models can be created just as if you were using approach 1 (shared database, shared schema).
- The easiest way to get this working is to embed the [postgres.TenantModel](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#TenantModel) struct in your tenant model. This will add the necessary fields for the tenant model (e.g. `DomainURL` and `SchemaName`), you can then create a foreign key constraint between the public and tenant specific models using the `SchemaName` field as the foreign key (e.g. `gorm:"foreignKey:TenantSchema;references:SchemaName"`); off course, you can also create foreign key constraints between any other fields in the models.

- Conforming to the [above conventions](#conventions), foreign key constraints between public and tenant specific models can be created just as if you were using approach 1 (shared database, shared schema).
- The easiest way to get this working is to embed the [postgres.TenantModel](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/drivers/postgres#TenantModel) struct in your tenant model. This will add the necessary fields for the tenant model (e.g. `DomainURL` and `SchemaName`), you can then create a foreign key constraint between the public and tenant specific models using the `SchemaName` field as the foreign key (e.g. `gorm:"foreignKey:TenantSchema;references:SchemaName"`); off course, you can also create foreign key constraints between any other fields in the models.

#### Operations on Tenant-Specific Models

Expand All @@ -84,7 +91,9 @@ Outlined below are two approaches to perform operations on tenant specific model
| [`SetSearchPath`](https://pkg.go.dev/github.com/bartventer/gorm-multitenancy/v5/schema/postgres#SetSearchPath) | Use this 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). |

#### Basic example
For a complete example refer to the [examples](#examples) section)

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

```go

import (
Expand All @@ -97,7 +106,7 @@ import (

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

Expand All @@ -106,12 +115,12 @@ func (t *Tenant) TableName() string {return "public.tenants"} // Note the public

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

// FK to TenantSchema (same as if you were using approach 1; not realy needed if you use
// approach 2 as the schema is constrained to the tenant already, but included to show how
// to create foreign key constraints between public a tenant specific models)
// FK to TenantSchema (same as if you were using approach 1; not realy needed if you use
// approach 2 as the schema is constrained to the tenant already, but included to show how
// to create foreign key constraints between public and tenant specific models)
TenantSchema string `gorm:"column:tenant_schema"`
Tenant Tenant `gorm:"foreignKey:TenantSchema;references:SchemaName"`

Expand All @@ -124,37 +133,21 @@ func (b *Book) TableName() string {return "books"} // Note the lack of prefix
func (b *Book) IsTenantTable() bool {return true} // This classifies the model as a tenant specific model

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",
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
if err := 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{},
); err != nil {
if err := postgres.RegisterModels(db, &Tenant{}, &Book{}); err != nil {
panic(err)
}

// 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",
Expand All @@ -165,125 +158,20 @@ func main(){
panic(err)
}

// Migrate the tenant schema
// This will create the schema and migrate all tenant specific models
if err := postgres.CreateSchemaForTenant(db, tenant.SchemaName); 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.SchemaName); err != nil {
panic(err)
}

// ... other operations
}
```

### echo middleware
For a complete example refer to the [PostgreSQL with echo](https://github.com/bartventer/gorm-multitenancy/tree/master/internal/examples/echo) example.
```go
import (
multitenancymw "github.com/bartventer/gorm-multitenancy/v5/middleware/echo"
"github.com/bartventer/gorm-multitenancy/v5/scopes"
"github.com/labstack/echo/v4"
// ...
)

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.WithTenantSchema(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](https://github.com/bartventer/gorm-multitenancy/tree/master/internal/examples/nethttp) example.
```go
import (
"encoding/json"
"net/http"
multitenancymw "github.com/bartventer/gorm-multitenancy/v5/middleware/nethttp"
"github.com/bartventer/gorm-multitenancy/v5/scopes"
// ...
)

type MiddlewareHandler struct {
handlerFunc func(http.ResponseWriter, *http.Request)
mw func(http.Handler) http.Handler
}

func (m MiddlewareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := m.mw(http.HandlerFunc(m.handlerFunc))
handler.ServeHTTP(w, r)
}

func main(){
// ...
mux := http.NewServeMux() // or use a router of your choice

// create tenant middleware
mw := 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,
})

// ... routes (using go 1.22 routing syntax)
mux.Handle("GET /books", MiddlewareHandler{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.WithTenantSchema(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
}
}, mw})

// ... rest of code
}

```

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

## Examples

- [PostgreSQL with echo](https://github.com/bartventer/gorm-multitenancy/tree/master/internal/examples/echo)
- [PostgreSQL with net/http](https://github.com/bartventer/gorm-multitenancy/tree/master/internal/examples/nethttp)
- [PostgreSQL with echo](https://github.com/bartventer/gorm-multitenancy/tree/master/examples/echo)
- [PostgreSQL with net/http](https://github.com/bartventer/gorm-multitenancy/tree/master/examples/nethttp)

## Contributing

Expand All @@ -294,4 +182,3 @@ All contributions are welcome! Open a pull request to request a feature or submi
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fbartventer%2Fgorm-multitenancy.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fbartventer%2Fgorm-multitenancy?ref=badge_large&issueType=license)

0 comments on commit 635db60

Please sign in to comment.