Skip to content

Commit

Permalink
feat: Add OpenDB function for blank import database initialization
Browse files Browse the repository at this point in the history
This update introduces the OpenDB method, enabling the opening of a database connection using a URL-like DSN string. This approach simplifies driver switching and abstracts driver mechanics, offering a unified, database-agnostic API. It enhances flexibility and ease of use in database connection processes, integrating seamlessly with GORM for a comprehensive ORM solution.
  • Loading branch information
bartventer committed Jul 6, 2024
1 parent 63ae6bd commit 753b0cb
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 48 deletions.
82 changes: 75 additions & 7 deletions adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/url"
"sync"

"github.com/bartventer/gorm-multitenancy/v8/pkg/gmterrors"
Expand All @@ -19,6 +20,12 @@ type Adapter interface {
// The returned [DB] instance is intended to be used by a single goroutine at a time,
// ensuring thread safety and avoiding concurrent access issues.
AdaptDB(ctx context.Context, db *gorm.DB) (*DB, error)

// OpenDBURL creates a new [DB] instance using the provided URL and returns it.
// It returns an error if the URL is invalid or if the adapter fails to open the database.
//
// The URL must be in a standard url format, as the scheme is used to determine the driver to use.
OpenDBURL(ctx context.Context, u *url.URL, opts ...gorm.Option) (*DB, error)
}

// driverMux acts as a registry for database driver openers, allowing dynamic driver management.
Expand Down Expand Up @@ -54,6 +61,23 @@ func (mux *driverMux) AdaptDB(ctx context.Context, db *gorm.DB) (*DB, error) {
return adapter.AdaptDB(ctx, db)
}

// OpenDB creates a new [DB] instance using the provided URL string and returns it.
// It returns an error if the URL is invalid or if the adapter fails to open the database.
func (mux *driverMux) OpenDB(ctx context.Context, urlstr string, opts ...gorm.Option) (*DB, error) {
u, err := url.Parse(urlstr)
if err != nil {
return nil, gmterrors.New(fmt.Errorf("failed to parse URL: %w", err))
}
driverName := u.Scheme
mux.mu.RLock()
adapter, ok := mux.drivers[driverName]
mux.mu.RUnlock()
if !ok {
return nil, gmterrors.New(errors.New("no registered adapter for driver: " + driverName))
}
return adapter.OpenDBURL(ctx, u, opts...)
}

var defaultDriverMux = new(driverMux)

// Register adds a new [Adapter] to the default registry under the specified driver name.
Expand All @@ -68,12 +92,15 @@ func Register(name string, adapter Adapter) {
// MySQL:
//
// import (
// "github.com/bartventer/gorm-multitenancy/mysql/v8"
// multitenancy "github.com/bartventer/gorm-multitenancy/v8"
// )
// "github.com/bartventer/gorm-multitenancy/mysql/v8"
// multitenancy "github.com/bartventer/gorm-multitenancy/v8"
// )
//
// dsn := "user:password@tcp(localhost:3306)/dbname?parseTime=True"
// db, err := multitenancy.Open(mysql.Open(dsn))
// func main() {
// dsn := "user:password@tcp(localhost:3306)/dbname?parseTime=True"
// db, err := multitenancy.Open(mysql.Open(dsn))
// if err != nil {...}
// }
//
// PostgreSQL:
//
Expand All @@ -82,12 +109,53 @@ func Register(name string, adapter Adapter) {
// multitenancy "github.com/bartventer/gorm-multitenancy/v8"
// )
//
// dsn := "postgres://user:password@localhost:5432/dbname?sslmode=disable"
// db, err := multitenancy.Open(postgres.Open(dsn))
// func main() {
// dsn := "postgres://user:password@localhost:5432/dbname?sslmode=disable"
// db, err := multitenancy.Open(postgres.Open(dsn))
// if err != nil {...}
// }
func Open(dialector gorm.Dialector, opts ...gorm.Option) (*DB, error) {
db, err := gorm.Open(dialector, opts...)
if err != nil {
return nil, gmterrors.New(fmt.Errorf("failed to open gorm database: %w", err))
}
return defaultDriverMux.AdaptDB(context.TODO(), db)
}

// OpenDB creates a new [DB] instance using the provided URL string and returns it.
//
// The URL string must be in a standard url format, as the scheme is used to determine
// the driver to use. The following are examples of valid URL strings for MySQL and
// PostgreSQL:
//
// "mysql://user:password@tcp(localhost:3306)/dbname"
// "postgres://user:password@localhost:5432/dbname"
//
// MySQL:
//
// import (
// _ "github.com/bartventer/gorm-multitenancy/mysql/v8"
// multitenancy "github.com/bartventer/gorm-multitenancy/v8"
// )
//
// func main() {
// url := "mysql://user:password@tcp(localhost:3306)/dbname"
// db, err := multitenancy.OpenDB(context.Background(), url)
// if err != nil {...}
// }
//
// PostgreSQL:
//
// import (
// _ "github.com/bartventer/gorm-multitenancy/postgres/v8"
// multitenancy "github.com/bartventer/gorm-multitenancy/v8"
// )
//
// func main() {
// url := "postgres://user:password@localhost:5432/dbname"
// db, err := multitenancy.OpenDB(context.Background(), url)
// if err != nil {...}
// }
func OpenDB(ctx context.Context, urlstr string) (*DB, error) {
return defaultDriverMux.OpenDB(ctx, urlstr)
}
79 changes: 78 additions & 1 deletion adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package multitenancy
import (
"context"
"errors"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -23,7 +24,15 @@ func (m *adapter) AdaptDB(ctx context.Context, db *gorm.DB) (*DB, error) {
return nil, nil
}

func TestDB(t *testing.T) {
// OpenDBURL implements Adapter.
func (m *adapter) OpenDBURL(ctx context.Context, u *url.URL, opts ...gorm.Option) (*DB, error) {
if u.Scheme == "err" {
return nil, errors.New("forced error")
}
return nil, nil
}

func TestAdaptDB(t *testing.T) {
ctx := context.Background()
mux := new(driverMux)

Expand Down Expand Up @@ -63,6 +72,61 @@ func TestDB(t *testing.T) {
}
}

func TestOpenDBURL(t *testing.T) {
ctx := context.Background()
mux := new(driverMux)

fake := &adapter{}
mux.Register("foo", fake)
mux.Register("err", fake)

for _, tc := range []struct {
name string
url string
wantErr bool
}{
{
name: "empty URL",
wantErr: true,
},
{
name: "invalid URL",
url: ":foo",
wantErr: true,
},
{
name: "invalid URL no scheme",
url: "foo",
wantErr: true,
},
{
name: "unregistered scheme",
url: "bar://mydb",
wantErr: true,
},
{
name: "func returns error",
url: "err://mydb",
wantErr: true,
},
{
name: "no query options",
url: "foo://mydb",
},
{
name: "empty query options",
url: "foo://mydb?",
},
} {
t.Run(tc.name, func(t *testing.T) {
_, gotErr := mux.OpenDB(ctx, tc.url)
if (gotErr != nil) != tc.wantErr {
t.Fatalf("got err %v, want error %v", gotErr, tc.wantErr)
}
})
}
}

func TestRegister(t *testing.T) {
fake := &adapter{}

Expand All @@ -86,6 +150,19 @@ func TestOpen(t *testing.T) {
assert.Error(t, err)
}

func TestOpenDB(t *testing.T) {
fake := &adapter{}
Register("foo2", fake)

// Test creating a new DB instance with a registered driver.
_, err := OpenDB(context.Background(), "foo2://mydb")
require.NoError(t, err)

// Test creating a new DB instance with an unregistered driver, should return an error.
_, err = OpenDB(context.Background(), "bar://mydb")
assert.Error(t, err)
}

// mockDialector is a mock implementation of gorm.Dialector for testing purposes.
var _ gorm.Dialector = new(mockDialector)

Expand Down
122 changes: 82 additions & 40 deletions multitenancy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,88 +20,130 @@ implementing multitenancy in Go applications.
# Opening a Database Connection
The package supports multitenancy for both PostgreSQL and MySQL databases.
The package supports multitenancy for both PostgreSQL and MySQL databases, offering three methods
for establishing a new database connection with multitenancy support:
Two methods are available for establishing a new database connection with multitenancy support:
# Approach 1: OpenDB with URL (Recommended for Most Users)
# Approach 1: Unified API
Utilize [Open] with a supported driver. The returned [*DB] instance not only provides a unified,
database-agnostic API for managing tenant-specific and shared data within a multi-tenant
application but also embeds the [gorm.DB] instance, thereby exposing all the functionality of GORM.
This approach is recommended for users seeking an integrated experience with multitenancy features,
allowing for seamless switching between database drivers. Starting from v8.0.0, this method is
recommended for new users.
[OpenDB] allows opening a database connection using a URL-like DSN string, providing a flexible
and easy way to switch between drivers. This method abstracts the underlying driver mechanics,
offering a straightforward connection process and a unified, database-agnostic API through the
returned [*DB] instance, which embeds the [gorm.DB] instance.
import (
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
"github.com/bartventer/gorm-multitenancy/<driver>/v8"
_ "github.com/bartventer/gorm-multitenancy/<driver>/v8"
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
)
func main() {
dsn := "<driver-specific DSN>"
db, err := multitenancy.Open(<driver>.Open(dsn))
if err != nil {...}
url := "<driver>://user:password@host:port/dbname"
db, err := multitenancy.OpenDB(context.Background(), url)
if err != nil {...}
db.RegisterModels(ctx, ...) // Access to a database-agnostic API with GORM features
}
This approach is useful for applications that need to dynamically switch between
different database drivers or configurations without changing the codebase.
Postgres:
import (
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
"github.com/bartventer/gorm-multitenancy/postgres/v8"
_ "github.com/bartventer/gorm-multitenancy/postgres/v8"
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
)
func main() {
dsn := "postgres://user:password@localhost:5432/dbname?sslmode=disable"
db, err := multitenancy.Open(postgres.Open(dsn))
url := "postgres://user:password@localhost:5432/dbname?sslmode=disable"
db, err := multitenancy.OpenDB(context.Background(), url)
if err != nil {...}
}
MySQL:
import (
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
"github.com/bartventer/gorm-multitenancy/mysql/v8"
_ "github.com/bartventer/gorm-multitenancy/mysql/v8"
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
)
func main() {
url := "mysql://user:password@tcp(localhost:3306)/dbname"
db, err := multitenancy.OpenDB(context.Background(), url)
if err != nil {...}
}
# Approach 2: Unified API
[Open] with a supported driver offers a unified, database-agnostic API for managing tenant-specific
and shared data, embedding the [gorm.DB] instance. This method facilitates seamless switching between
database drivers while maintaining access to GORM's full functionality.
import (
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
"github.com/bartventer/gorm-multitenancy/<driver>/v8"
)
func main() {
dsn := "user:password@tcp(localhost:3306)/dbname"
db, err := multitenancy.Open(mysql.Open(dsn))
dsn := "<driver-specific DSN>"
db, err := multitenancy.Open(<driver>.Open(dsn))
if err != nil {...}
db.RegisterModels(ctx, ...) // Access to a database-agnostic API with GORM features
}
# Approach 2: Direct Driver API
Postgres:
import (
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
"github.com/bartventer/gorm-multitenancy/postgres/v8"
)
For users who prefer the [gorm.DB] API for its direct access and only need multitenancy features
for specific tasks, this approach allows the direct invocation of driver-specific functions.
Initially, until the release of v8.0.0, it was the exclusive method for interacting with the
framework. However, it's important to note that opting for this method entails managing
database-specific operations manually, offering a lower level of abstraction compared to what the
unified API provides.
func main() {
dsn := "postgres://user:password@localhost:5432/dbname?sslmode=disable"
db, err := multitenancy.Open(postgres.Open(dsn))
}
MySQL:
import (
multitenancy "github.com/bartventer/gorm-multitenancy/v8"
"github.com/bartventer/gorm-multitenancy/mysql/v8"
)
func main() {
dsn := "user:password@tcp(localhost:3306)/dbname"
db, err := multitenancy.Open(mysql.Open(dsn))
}
# Approach 3: Direct Driver API
For direct access to the [gorm.DB] API and multitenancy features for specific tasks, this approach
allows invoking driver-specific functions directly. It provides a lower level of abstraction,
requiring manual management of database-specific operations. Prior to version 8, this was the
only method available for using the package.
Postgres:
import (
"github.com/bartventer/gorm-multitenancy/postgres/v8"
"gorm.io/gorm"
"github.com/bartventer/gorm-multitenancy/postgres/v8"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Directly call driver-specific functions
postgres.RegisterModels(db, ...)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// Directly call driver-specific functions
postgres.RegisterModels(db, ...)
}
MySQL:
import (
"github.com/bartventer/gorm-multitenancy/mysql/v8"
"gorm.io/gorm"
"github.com/bartventer/gorm-multitenancy/mysql/v8"
"gorm.io/gorm"
)
func main() {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// Directly call driver-specific functions
mysql.RegisterModels(db, ...)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// Directly call driver-specific functions
mysql.RegisterModels(db, ...)
}
# Declaring Models
Expand Down
Loading

0 comments on commit 753b0cb

Please sign in to comment.