From ae9d8b94095d57b5295eb1f9c4577f0191b8ae60 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Tue, 19 Dec 2023 11:03:06 +0530 Subject: [PATCH 1/6] feat: add wrapper function to support MongoDB transactions --- .golangci.yml | 4 +- README.md | 6 ++- connection.go | 16 ++++--- docs/README.md | 3 +- docs/basic_usage.md | 6 ++- docs/transactions.md | 60 +++++++++++++++++++++++++ transaction.go | 33 ++++++++++++++ transaction_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 docs/transactions.md create mode 100644 transaction.go create mode 100644 transaction_test.go diff --git a/.golangci.yml b/.golangci.yml index 9e9b000..cea9bdf 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -243,7 +243,6 @@ linters: - nilnil # checks that there is no simultaneous return of nil error and an invalid value - noctx # finds sending http request without context.Context - nolintlint # reports ill-formed or insufficient nolint directives - - nonamedreturns # reports all named returns - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - predeclared # finds code that shadows one of Go's predeclared identifiers - promlinter # checks Prometheus metrics naming via promlint @@ -269,8 +268,9 @@ linters: #- decorder # checks declaration order and count of types, constants, variables and functions #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega #- goheader # checks is file header matches to pattern - # - godox # detects FIXME, TODO and other comment keywords + #- godox # detects FIXME, TODO and other comment keywords #- ireturn # accept interfaces, return concrete types + #- nonamedreturns # reports all named returns #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated #- tagalign # checks that struct tags are well aligned #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope diff --git a/README.md b/README.md index 2f6a0de..4e46f04 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,10 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - // dbConn is the database connection obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(dbConn) + dbName := "mgod-test" + + // client is the MongoDB client obtained using Go Mongo Driver's Connect method. + mgod.SetDefaultConnection(client, dbName) } ``` diff --git a/connection.go b/connection.go index d6aab0c..818204b 100644 --- a/connection.go +++ b/connection.go @@ -8,27 +8,29 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +var mClient *mongo.Client var dbConn *mongo.Database var defaultTimeout = 10 * time.Second // ConnectionConfig is the configuration options available for a MongoDB connection. type ConnectionConfig struct { - // Timeout is the timeout for various operations performed on the MongoDB server like Connect, Ping, Session etc. + // Timeout is the timeout for various operations performed on the MongoDB server like Connect, Ping etc. Timeout time.Duration } // SetDefaultConnection sets the default connection to be used by the package. -func SetDefaultConnection(conn *mongo.Database) { - dbConn = conn +func SetDefaultConnection(client *mongo.Client, dbName string) { + mClient = client + dbConn = mClient.Database(dbName) } // ConfigureDefaultConnection opens a new connection using the provided config options and sets it as a default connection to be used by the package. -func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*options.ClientOptions) error { +func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*options.ClientOptions) (err error) { if cfg == nil { cfg = defaultConnectionConfig() } - client, err := newClient(cfg, opts...) + mClient, err = newClient(cfg, opts...) if err != nil { return err } @@ -37,12 +39,12 @@ func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*o defer cancel() // Ping the MongoDB server to check if connection is established. - err = client.Ping(ctx, nil) + err = mClient.Ping(ctx, nil) if err != nil { return err } - dbConn = client.Database(dbName) + dbConn = mClient.Database(dbName) return nil } diff --git a/docs/README.md b/docs/README.md index 94d2408..45bce26 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,4 +18,5 @@ This directory contains user-facing documentation. For those who wish to underst * [Meta fields](./meta_fields.md) ### Advanced Guide -* [Unions](./union_types.md) \ No newline at end of file +* [Unions](./union_types.md) +* [Transactions](./transactions.md) \ No newline at end of file diff --git a/docs/basic_usage.md b/docs/basic_usage.md index c2cc8a9..d42df14 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -9,8 +9,10 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - // dbConn is the database connection obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(dbConn) + dbName := "mgod-test" + + // client is the MongoDB client obtained using Go Mongo Driver's Connect method. + mgod.SetDefaultConnection(client, dbName) } ``` diff --git a/docs/transactions.md b/docs/transactions.md new file mode 100644 index 0000000..8ce9c10 --- /dev/null +++ b/docs/transactions.md @@ -0,0 +1,60 @@ +--- +title: Transactions +--- + +`mgod` provides a wrapper function `WithTransaction` that supports MongoDB transactions, allowing users to perform a series of read and write operations as a single atomic unit. + +## Usage + +Configure default connection with `mgod`. + +```go +cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} +dbName := "mgod_test" +opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") + +err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) +``` + +:::info +To use Transactions, it is compulsory to run MongoDB daemon as a replica set. +Refer Community Forum Discussion - [Why replica set is mandatory for transactions in MongoDB?](https://www.mongodb.com/community/forums/t/why-replica-set-is-mandatory-for-transactions-in-mongodb/9533) +::: + +Create models to be used inside a MongoDB transaction. + +```go +type User struct { + Name string + EmailID string `bson:"emailId"` +} + +schemaOpts := schemaopt.SchemaOptions{ + Collection: "users", + Timestamps: true, +} + +userModel, _ := mgod.NewEntityMongoModel(User{}, schemaOpts) +``` + +Use `WithTransaction` function to perform multiple CRUD operations as an atomic unit. + +```go +userDoc1 := User{Name: "Gopher1", EmailID: "gopher1@mgod.com"} +userDoc2 := User{Name: "Gopher2", EmailID: "gopher2@mgod.com"} + +_, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + _, err1 := s.userModel.InsertOne(sc, userDoc1) + _, err2 := s.userModel.InsertOne(sc, userDoc2) + + if err1 != nil || err2 != nil { + return nil, errors.New("abort transaction") + } + + return nil, nil +}) +``` + +:::warning +Make sure to pass the session's context (`sc` here) only in EntityMongoModel's operation functions. +::: \ No newline at end of file diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..bd5c248 --- /dev/null +++ b/transaction.go @@ -0,0 +1,33 @@ +package mgod + +import ( + "context" + "log/slog" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" +) + +// TransactionFunc is the function that is executed in a MongoDB transaction. +// +// SessionContext(sc) combines the context.Context and mongo.Session interfaces. +type TransactionFunc func(sc mongo.SessionContext) (interface{}, error) + +// WithTransaction executes the given transaction function with a new session. +func WithTransaction(ctx context.Context, transactionFunc TransactionFunc) (interface{}, error) { + session, err := mClient.StartSession() + if err != nil { + slog.ErrorContext(ctx, "Error occurred during WithTransaction", err) + return nil, err + } + defer session.EndSession(ctx) + + // Reason behind using read preference: + // https://www.mongodb.com/community/forums/t/why-can-t-read-preference-be-secondary-in-a-transaction/204432 + payload, transactionErr := session.WithTransaction(ctx, transactionFunc, &options.TransactionOptions{ + ReadPreference: readpref.Primary(), + }) + + return payload, transactionErr +} diff --git a/transaction_test.go b/transaction_test.go new file mode 100644 index 0000000..02f2cff --- /dev/null +++ b/transaction_test.go @@ -0,0 +1,103 @@ +package mgod_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/Lyearn/mgod" + "github.com/Lyearn/mgod/schema/schemaopt" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type TransactionSuite struct { + suite.Suite + *require.Assertions + + userModel mgod.EntityMongoModel[TransactionTestUser] +} + +type TransactionTestUser struct { + Name string + EmailID string `bson:"emailId"` +} + +func TestTransactionSuite(t *testing.T) { + s := new(TransactionSuite) + suite.Run(t, s) +} + +func (s *TransactionSuite) SetupTest() { + s.Assertions = require.New(s.T()) + s.SetupConnectionAndModel() +} + +func (s *TransactionSuite) SetupConnectionAndModel() { + cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} + dbName := "mgod_test" + opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") + + err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + if err != nil { + s.T().Fatal(err) + } + + schemaOpts := schemaopt.SchemaOptions{ + Collection: "users", + Timestamps: true, + } + userModel, err := mgod.NewEntityMongoModel(TransactionTestUser{}, schemaOpts) + if err != nil { + s.T().Fatal(err) + } + + s.userModel = userModel +} + +func (s *TransactionSuite) TestWithTransaction() { + userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + + p, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + user, err := s.userModel.InsertOne(sc, userDoc) + return user, err + }) + + user, ok := p.(TransactionTestUser) + + s.True(ok) + s.NoError(err) + s.Equal(user.Name, userDoc.Name) + s.Equal(user.EmailID, userDoc.EmailID) + + userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + + s.NoError(err) + s.Equal(userCount, int64(1)) +} + +func (s *TransactionSuite) TestWithTransactionAbort() { + userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + + abortErr := errors.New("dummy error to abort transaction") + + _, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + _, err := s.userModel.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } + + return nil, abortErr + }) + + s.EqualError(err, abortErr.Error()) + + userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + + s.NoError(err) + s.Equal(userCount, int64(0)) +} From 86145c857eebeaa10b34e8cc56a713515d9bf23c Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Tue, 19 Dec 2023 11:06:16 +0530 Subject: [PATCH 2/6] doc: mark transactions feature as done in future scope section --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e46f04..5715409 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,9 @@ Inspired by the easy interface of MongoDB handling using [Mongoose](https://gith ## Future Scope The current version of mgod is a stable release. However, there are plans to add a lot more features like - -- [ ] Enable functionality to opt out of the default conversion of date fields to ISOString format. - [x] Implement a setup step for storing a default Mongo connection, eliminating the need to pass it during EntityMongoModel creation. -- [ ] Provide support for transactions following the integration of default Mongo connection logic. +- [x] Provide support for transactions following the integration of default Mongo connection logic. +- [ ] Enable functionality to opt out of the default conversion of date fields to ISOString format. - [ ] Develop easy to use wrapper functions around MongoDB Aggregation operation. - [ ] Introduce automatic MongoDB collection selection based on Go struct names as a default behavior. - [ ] Add test cases to improve code coverage. From a476567e4d1c68d74a61d7687e1fd928f6fb7081 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 12:24:22 +0530 Subject: [PATCH 3/6] fix: support multi tenancy --- README.md | 13 +- connection.go | 12 +- connection_cache.go | 21 +++ docs/README.md | 2 +- docs/basic_usage.md | 12 +- docs/meta_fields.md | 7 +- docs/schema_options.md | 19 --- docs/transactions.md | 10 +- entity_mongo_model.go | 10 +- entity_mongo_model_opts.go | 31 ++++ entity_mongo_model_test.go | 256 ++++++++++++----------------- schema/schemaopt/schema_options.go | 4 - transaction_test.go | 110 ++++++++++--- 13 files changed, 271 insertions(+), 236 deletions(-) create mode 100644 connection_cache.go create mode 100644 entity_mongo_model_opts.go diff --git a/README.md b/README.md index 5715409..48a29ea 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - Easily manage **meta fields** in models without cluttering Go structs. - Supports **union types**, expanding data capabilities. - Implement strict field requirements with struct tags for **data integrity**. +- Built-in support for **multi-tenant** systems. - Wrapper around the **official** Mongo Go Driver. ## Requirements @@ -42,10 +43,8 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - dbName := "mgod-test" - // client is the MongoDB client obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(client, dbName) + mgod.SetDefaultClient(client) } ``` @@ -61,10 +60,9 @@ import ( func init() { // `cfg` is optional. Can rely on default configurations by providing `nil` value in argument. cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} - dbName := "mgod-test" opts := options.Client().ApplyURI("mongodb://root:mgod123@localhost:27017") - err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + err := mgod.ConfigureDefaultClient(cfg, opts) } ``` @@ -86,11 +84,14 @@ import ( ) model := User{} +dbName := "mgoddb" +collection := "users" + schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } +opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) ``` diff --git a/connection.go b/connection.go index 818204b..6b0ac10 100644 --- a/connection.go +++ b/connection.go @@ -9,7 +9,6 @@ import ( ) var mClient *mongo.Client -var dbConn *mongo.Database var defaultTimeout = 10 * time.Second // ConnectionConfig is the configuration options available for a MongoDB connection. @@ -18,14 +17,13 @@ type ConnectionConfig struct { Timeout time.Duration } -// SetDefaultConnection sets the default connection to be used by the package. -func SetDefaultConnection(client *mongo.Client, dbName string) { +// SetDefaultClient sets the default MongoDB client to be used by the package. +func SetDefaultClient(client *mongo.Client, dbName string) { mClient = client - dbConn = mClient.Database(dbName) } -// ConfigureDefaultConnection opens a new connection using the provided config options and sets it as a default connection to be used by the package. -func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*options.ClientOptions) (err error) { +// ConfigureDefaultClient opens a new connection using the provided config options and sets the default MongoDB client to be used by the package. +func ConfigureDefaultClient(cfg *ConnectionConfig, opts ...*options.ClientOptions) (err error) { if cfg == nil { cfg = defaultConnectionConfig() } @@ -44,8 +42,6 @@ func ConfigureDefaultConnection(cfg *ConnectionConfig, dbName string, opts ...*o return err } - dbConn = mClient.Database(dbName) - return nil } diff --git a/connection_cache.go b/connection_cache.go new file mode 100644 index 0000000..cc8ed96 --- /dev/null +++ b/connection_cache.go @@ -0,0 +1,21 @@ +package mgod + +import "go.mongodb.org/mongo-driver/mongo" + +// dbConnCache is a cache of MongoDB database connections. +var dbConnCache map[string]*mongo.Database + +func init() { + dbConnCache = make(map[string]*mongo.Database) +} + +// getDBConn returns a MongoDB database connection from the cache. +// If the connection is not present in the cache, it creates a new connection and adds it to the cache (Write-through policy). +func getDBConn(dbName string) *mongo.Database { + // Initialize the cache entry if it is not present. + if dbConnCache[dbName] == nil { + dbConnCache[dbName] = mClient.Database(dbName) + } + + return dbConnCache[dbName] +} diff --git a/docs/README.md b/docs/README.md index 45bce26..c06c3a3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,4 +19,4 @@ This directory contains user-facing documentation. For those who wish to underst ### Advanced Guide * [Unions](./union_types.md) -* [Transactions](./transactions.md) \ No newline at end of file +* [Transactions](./transactions.md) diff --git a/docs/basic_usage.md b/docs/basic_usage.md index d42df14..b081489 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -9,10 +9,8 @@ For existing database connection, import "github.com/Lyearn/mgod" func init() { - dbName := "mgod-test" - // client is the MongoDB client obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultConnection(client, dbName) + mgod.SetDefaultClient(client, dbName) } ``` @@ -28,10 +26,9 @@ import ( func init() { // `cfg` is optional. Can rely on default configurations by providing `nil` value in argument. cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} - dbName := "mgod-test" opts := options.Client().ApplyURI("mongodb://root:mgod123@localhost:27017") - err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + err := mgod.ConfigureDefaultClient(cfg, opts) } ``` @@ -59,11 +56,14 @@ import ( ) model := User{} +dbName := "mgoddb" +collection := "users" + schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } +opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) ``` diff --git a/docs/meta_fields.md b/docs/meta_fields.md index 021de52..1b1c4ea 100644 --- a/docs/meta_fields.md +++ b/docs/meta_fields.md @@ -25,7 +25,6 @@ It is the meta field that stores the timestamp of the document creation. This fi ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } @@ -33,6 +32,7 @@ userDoc := User{ Name: "Gopher", EmailID: "gopher@mgod.com", } + user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` @@ -59,7 +59,6 @@ It is the meta field that stores the timestamp of the document updation. This fi ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } @@ -98,7 +97,6 @@ It is the field that stores the version of the document. This field is automatic ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", VersionKey: true } @@ -106,6 +104,7 @@ userDoc := User{ Name: "Gopher", EmailID: "gopher@mgod.com", } + user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` @@ -124,7 +123,6 @@ If `VersionKey` is set to `false`. ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", VersionKey: false } @@ -132,6 +130,7 @@ userDoc := User{ Name: "Gopher", EmailID: "gopher@mgod.com", } + user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` diff --git a/docs/schema_options.md b/docs/schema_options.md index dbd18d7..41b09ff 100644 --- a/docs/schema_options.md +++ b/docs/schema_options.md @@ -6,21 +6,6 @@ Schema Options is Mongo Schema level options (which modifies actual MongoDB doc) `mgod` supports the following schema options - -## Collection - -- Accepts Type: `string` -- Is Optional: `No` - -It is the name of the mongo collection in which the entity is stored. For example, `users` collection of MongoDB for `User` model in Golang. - -### Usage - -```go -schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", // MongoDB collection name -} -``` - ## Timestamps - Accepts Type: `bool` @@ -33,7 +18,6 @@ It is used to track `createdAt` and `updatedAt` meta fields for the entity. See ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } ``` @@ -50,7 +34,6 @@ This reports whether to add a version key (`__v`) for the entity. See [Meta Fiel ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", VersionKey: true, } ``` @@ -67,7 +50,6 @@ It defines whether the entity is a union type. See [Union Types](union_types.md) ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "resources", IsUnionType: true, } ``` @@ -90,7 +72,6 @@ It is the key used to identify the underlying type in case of a union type entit ```go schemaOpts := schemaopt.SchemaOptions{ - Collection: "resources", IsUnionType: true, DiscriminatorKey: "type", } diff --git a/docs/transactions.md b/docs/transactions.md index 8ce9c10..171a8b2 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -10,10 +10,9 @@ Configure default connection with `mgod`. ```go cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} -dbName := "mgod_test" opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") -err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) +err := mgod.ConfigureDefaultClient(cfg, opts) ``` :::info @@ -29,12 +28,13 @@ type User struct { EmailID string `bson:"emailId"` } +dbName := "mgoddb" +collection := "users" schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", Timestamps: true, } -userModel, _ := mgod.NewEntityMongoModel(User{}, schemaOpts) +userModel, _ := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) ``` Use `WithTransaction` function to perform multiple CRUD operations as an atomic unit. @@ -57,4 +57,4 @@ _, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContex :::warning Make sure to pass the session's context (`sc` here) only in EntityMongoModel's operation functions. -::: \ No newline at end of file +::: diff --git a/entity_mongo_model.go b/entity_mongo_model.go index e40d8c5..dcdb48c 100644 --- a/entity_mongo_model.go +++ b/entity_mongo_model.go @@ -74,12 +74,13 @@ type entityMongoModel[T any] struct { } // NewEntityMongoModel returns a new instance of EntityMongoModel for the provided model type and options. -func NewEntityMongoModel[T any](modelType T, schemaOpts schemaopt.SchemaOptions) (EntityMongoModel[T], error) { +func NewEntityMongoModel[T any](modelType T, opts entityMongoModelOptions) (EntityMongoModel[T], error) { + dbConn := getDBConn(opts.connOpts.db) if dbConn == nil { return nil, errors.ErrNoDatabaseConnection } - coll := dbConn.Collection(schemaOpts.Collection) + coll := dbConn.Collection(opts.connOpts.coll) modelName := schema.GetSchemaNameForModel(modelType) schemaCacheKey := GetSchemaCacheKey(coll.Name(), modelName) @@ -87,6 +88,11 @@ func NewEntityMongoModel[T any](modelType T, schemaOpts schemaopt.SchemaOptions) var entityModelSchema *schema.EntityModelSchema var err error + schemaOpts := schemaopt.SchemaOptions{} + if opts.schemaOpts != nil { + schemaOpts = *opts.schemaOpts + } + // build schema if not cached. if entityModelSchema, err = schema.EntityModelSchemaCacheInstance.GetSchema(schemaCacheKey); err != nil { entityModelSchema, err = schema.BuildSchemaForModel(modelType, schemaOpts) diff --git a/entity_mongo_model_opts.go b/entity_mongo_model_opts.go new file mode 100644 index 0000000..d0fa926 --- /dev/null +++ b/entity_mongo_model_opts.go @@ -0,0 +1,31 @@ +package mgod + +import ( + "github.com/Lyearn/mgod/schema/schemaopt" +) + +type entityMongoModelOptions struct { + connOpts connectionOptions + schemaOpts *schemaopt.SchemaOptions +} + +type connectionOptions struct { + db string + coll string +} + +// NewEntityMongoModelOptions creates a new entityMongoModelOptions instance. +// Its instance is used to provide necessary configuration options to the NewEntityMongoModel function. +// +// dbName is the name of the database in which the entity is stored. +// collection is the name of the mongo collection in which the entity is stored. +// schemaOpts is the schema level options for the entity. +func NewEntityMongoModelOptions(dbName string, collection string, schemaOpts *schemaopt.SchemaOptions) *entityMongoModelOptions { + return &entityMongoModelOptions{ + connOpts: connectionOptions{ + db: dbName, + coll: collection, + }, + schemaOpts: schemaOpts, + } +} diff --git a/entity_mongo_model_test.go b/entity_mongo_model_test.go index fc3948c..f0bf1bc 100644 --- a/entity_mongo_model_test.go +++ b/entity_mongo_model_test.go @@ -6,198 +6,146 @@ import ( "time" "github.com/Lyearn/mgod" - "github.com/Lyearn/mgod/dateformatter" "github.com/Lyearn/mgod/schema/schemaopt" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/integration/mtest" + "go.mongodb.org/mongo-driver/mongo/options" ) type EntityMongoModelSuite struct { suite.Suite *require.Assertions +} - dbName string - collName string - - mt *mtest.T - mtOpts *mtest.Options +type testEntity struct { + ID string `bson:"_id" mgoType:"id"` + Name string + Age *int `bson:",omitempty" mgoDefault:"18"` } func TestEntityMongoModelSuite(t *testing.T) { s := new(EntityMongoModelSuite) - s.dbName = "foo" - s.collName = "bar" - suite.Run(t, s) } +func (s *EntityMongoModelSuite) SetupSuite() { + s.setupConnection() + s.setupData() +} + func (s *EntityMongoModelSuite) SetupTest() { s.Assertions = require.New(s.T()) +} - mtOpts := mtest.NewOptions() - mtOpts = mtOpts.ClientType(mtest.Mock) - mtOpts = mtOpts.DatabaseName(s.dbName) - mtOpts = mtOpts.CollectionName(s.collName) +func (s *EntityMongoModelSuite) TearDownSuite() { + entityMongoModel := s.getModel() + _, err := entityMongoModel.DeleteMany(context.Background(), bson.D{}) + if err != nil { + s.T().Fatal(err) + } +} - mt := mtest.New(s.T(), mtOpts) +func (s *EntityMongoModelSuite) setupConnection() { + cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} + uri := "mongodb://localhost:27017/?replicaSet=replset&authSource=admin" - s.mt = mt - s.mtOpts = mtOpts + err := mgod.ConfigureDefaultClient(cfg, options.Client().ApplyURI(uri)) + if err != nil { + s.T().Fatal(err) + } } -func (s *EntityMongoModelSuite) ns() string { - return s.dbName + "." + s.collName +func (s *EntityMongoModelSuite) setupData() { + firstID := primitive.NewObjectID() + secondID := primitive.NewObjectID() + + age1 := 30 + age2 := 40 + + entities := []testEntity{ + { + ID: firstID.Hex(), + Name: "Default User 1", + Age: &age1, + }, + { + ID: secondID.Hex(), + Name: "Default User 2", + Age: &age2, + }, + } + + entityMongoModel := s.getModel() + _, err := entityMongoModel.InsertMany(context.Background(), entities) + if err != nil { + s.T().Fatal(err) + } } -type TestEntity struct { - ID string `bson:"_id" mgoType:"id"` - Name string - JoinedOn string `mgoType:"date"` - Age *int `bson:",omitempty" mgoDefault:"18"` +func (s *EntityMongoModelSuite) getModel() mgod.EntityMongoModel[testEntity] { + schemaOpts := schemaopt.SchemaOptions{Timestamps: true} + opts := mgod.NewEntityMongoModelOptions("mgoddb", "entityMongoModel", &schemaOpts) + model, err := mgod.NewEntityMongoModel(testEntity{}, *opts) + if err != nil { + s.T().Fatal(err) + } + + return model } func (s *EntityMongoModelSuite) TestFind() { - defer s.mt.Close() - - s.mt.RunOpts("find", s.mtOpts, func(mt *mtest.T) { - currentTime := time.Now() - currentTimeStr, _ := dateformatter.New(currentTime).GetISOString() - - firstID := primitive.NewObjectID() - secondID := primitive.NewObjectID() - - //nolint:govet // this is a mock entity. - firstEntity := TestEntity{ - ID: firstID.Hex(), - Name: "test1", - JoinedOn: currentTimeStr, - } - //nolint:govet // this is a mock entity. - secondEntity := TestEntity{ - ID: secondID.Hex(), - Name: "test2", - JoinedOn: currentTimeStr, - } - - first := mtest.CreateCursorResponse(1, s.ns(), mtest.FirstBatch, bson.D{ - {Key: "_id", Value: firstID}, - {Key: "name", Value: firstEntity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - }) - second := mtest.CreateCursorResponse(1, s.ns(), mtest.NextBatch, bson.D{ - {Key: "_id", Value: secondID}, - {Key: "name", Value: secondEntity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - }) - killCursors := mtest.CreateCursorResponse(0, s.ns(), mtest.NextBatch) - - mt.AddMockResponses(first, second, killCursors) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - testEntities, err := entityMongoModel.Find(context.Background(), bson.D{ - {Key: "name", Value: firstEntity.Name}, - }) - - s.Nil(err) - s.Equal(2, len(testEntities)) + entityMongoModel := s.getModel() + entities, err := entityMongoModel.Find(context.Background(), bson.M{ + "age": bson.M{ + "$gt": 20, + }, + "name": bson.M{ + "$regex": "Default", + }, }) + + s.NoError(err) + s.Equal(2, len(entities)) } func (s *EntityMongoModelSuite) TestFindOne() { - defer s.mt.Close() - - s.mt.RunOpts("find one", s.mtOpts, func(mt *mtest.T) { - currentTime := time.Now() - currentTimeStr, _ := dateformatter.New(currentTime).GetISOString() - - id := primitive.NewObjectID() - - //nolint:govet // this is a mock entity. - entity := TestEntity{ - ID: id.Hex(), - Name: "test", - JoinedOn: currentTimeStr, - } - - mt.AddMockResponses(mtest.CreateCursorResponse(1, s.ns(), mtest.FirstBatch, bson.D{ - {Key: "_id", Value: id}, - {Key: "name", Value: entity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - })) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - testEntity, err := entityMongoModel.FindOne(context.Background(), bson.D{ - {Key: "id", Value: entity.ID}, - }) - - s.Nil(err) - s.Equal(entity.ID, testEntity.ID) + entityMongoModel := s.getModel() + entity, err := entityMongoModel.FindOne(context.Background(), bson.M{ + "age": bson.M{ + "$gt": 30, + }, + "name": bson.M{ + "$regex": "Default", + }, }) + + s.NoError(err) + s.Equal("Default User 2", entity.Name) } func (s *EntityMongoModelSuite) TestInsertOne() { - defer s.mt.Close() - id := primitive.NewObjectID() - currentTime := time.Now() - currentTimeStr, _ := dateformatter.New(currentTime).GetISOString() - - s.mt.RunOpts("insert one", s.mtOpts, func(mt *mtest.T) { - entity := TestEntity{ - ID: id.Hex(), - Name: "test", - JoinedOn: currentTimeStr, - } - - mt.AddMockResponses(mtest.CreateCursorResponse(1, s.ns(), mtest.FirstBatch, bson.D{ - {Key: "_id", Value: id}, - {Key: "name", Value: entity.Name}, - {Key: "joinedon", Value: primitive.NewDateTimeFromTime(currentTime)}, - {Key: "age", Value: 18}, - })) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - doc, err := entityMongoModel.InsertOne(context.Background(), entity) - - s.Nil(err) - s.Equal(entity.ID, doc.ID) - s.Equal(18, *doc.Age) - }) - - s.mt.RunOpts("insert one with error", s.mtOpts, func(mt *mtest.T) { - entity := TestEntity{ - ID: id.Hex(), - Name: "test", - JoinedOn: currentTimeStr, - } - - mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{ - Index: 1, - Code: 11000, - Message: "duplicate key error", - })) - - opts := schemaopt.SchemaOptions{Collection: s.collName} - entityMongoModel, err := mgod.NewEntityMongoModel(TestEntity{}, opts) - s.Nil(err) - - docID, err := entityMongoModel.InsertOne(context.Background(), entity) - - s.Empty(docID) - s.NotNil(err) - s.True(mongo.IsDuplicateKeyError(err)) - }) + age := 18 + + entity := testEntity{ + ID: id.Hex(), + Name: "test", + Age: &age, + } + + entityMongoModel := s.getModel() + doc, err := entityMongoModel.InsertOne(context.Background(), entity) + + s.Nil(err) + s.Equal(entity.ID, doc.ID) + s.Equal(18, *doc.Age) + + // Test duplicate key error + docID, err := entityMongoModel.InsertOne(context.Background(), entity) + s.Empty(docID) + s.NotNil(err) + s.True(mongo.IsDuplicateKeyError(err)) } diff --git a/schema/schemaopt/schema_options.go b/schema/schemaopt/schema_options.go index cb68773..02c2f1d 100644 --- a/schema/schemaopt/schema_options.go +++ b/schema/schemaopt/schema_options.go @@ -1,11 +1,7 @@ package schemaopt // SchemaOptions is Mongo Schema level options (modifies actual MongoDB doc) that needs to be provided when creating a new EntityMongoModel. -// -// These options are used to identify the collection name, whether to add timestamps meta fields, etc. type SchemaOptions struct { - // Collection is the name of the mongo collection in which the entity is stored. - Collection string // Timestamps reports whether to add createdAt and updatedAt meta fields for the entity. Timestamps bool // VersionKey reports whether to add a version key for the entity. Defaults to true. diff --git a/transaction_test.go b/transaction_test.go index 02f2cff..f542031 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -3,6 +3,7 @@ package mgod_test import ( "context" "errors" + "fmt" "testing" "time" @@ -18,11 +19,9 @@ import ( type TransactionSuite struct { suite.Suite *require.Assertions - - userModel mgod.EntityMongoModel[TransactionTestUser] } -type TransactionTestUser struct { +type transactionTestUser struct { Name string EmailID string `bson:"emailId"` } @@ -32,61 +31,74 @@ func TestTransactionSuite(t *testing.T) { suite.Run(t, s) } +func (s *TransactionSuite) SetupSuite() { + s.setupConnection() +} + func (s *TransactionSuite) SetupTest() { s.Assertions = require.New(s.T()) - s.SetupConnectionAndModel() } -func (s *TransactionSuite) SetupConnectionAndModel() { +func (s *TransactionSuite) setupConnection() { cfg := &mgod.ConnectionConfig{Timeout: 5 * time.Second} - dbName := "mgod_test" - opts := options.Client().ApplyURI("mongodb://localhost:27017/?replicaSet=mgod_rs&authSource=admin") + // Can use the `mlaunch` tool to start a local replica set using command `mlaunch --repl`. + uri := "mongodb://localhost:27017/?replicaSet=replset&authSource=admin" - err := mgod.ConfigureDefaultConnection(cfg, dbName, opts) + err := mgod.ConfigureDefaultClient(cfg, options.Client().ApplyURI(uri)) if err != nil { s.T().Fatal(err) } +} - schemaOpts := schemaopt.SchemaOptions{ - Collection: "users", - Timestamps: true, - } - userModel, err := mgod.NewEntityMongoModel(TransactionTestUser{}, schemaOpts) +func (s *TransactionSuite) getModelForDB(dbName string) mgod.EntityMongoModel[transactionTestUser] { + schemaOpts := schemaopt.SchemaOptions{Timestamps: true} + opts := mgod.NewEntityMongoModelOptions(dbName, "users", &schemaOpts) + userModel, err := mgod.NewEntityMongoModel(transactionTestUser{}, *opts) if err != nil { s.T().Fatal(err) } - s.userModel = userModel + return userModel } func (s *TransactionSuite) TestWithTransaction() { - userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + userModel := s.getModelForDB("mgod1") + userDoc := transactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} p, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { - user, err := s.userModel.InsertOne(sc, userDoc) - return user, err - }) + _, err := userModel.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } - user, ok := p.(TransactionTestUser) + userCount, err := userModel.CountDocuments(sc, bson.M{}) + if err != nil { + return nil, err + } - s.True(ok) - s.NoError(err) - s.Equal(user.Name, userDoc.Name) - s.Equal(user.EmailID, userDoc.EmailID) + _, err = userModel.DeleteOne(sc, bson.M{}) + if err != nil { + return nil, err + } - userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + return userCount, nil + }) + + userCount, ok := p.(int64) s.NoError(err) + s.True(ok) s.Equal(userCount, int64(1)) } func (s *TransactionSuite) TestWithTransactionAbort() { - userDoc := TransactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + userModel := s.getModelForDB("mgod1") + userDoc := transactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} abortErr := errors.New("dummy error to abort transaction") _, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { - _, err := s.userModel.InsertOne(sc, userDoc) + _, err := userModel.InsertOne(sc, userDoc) if err != nil { return nil, err } @@ -96,8 +108,52 @@ func (s *TransactionSuite) TestWithTransactionAbort() { s.EqualError(err, abortErr.Error()) - userCount, err := s.userModel.CountDocuments(context.Background(), bson.M{}) + userCount, err := userModel.CountDocuments(context.Background(), bson.M{}) s.NoError(err) s.Equal(userCount, int64(0)) } + +func (s *TransactionSuite) TestWithTransactionForMultiTenancy() { + userModelTenant1 := s.getModelForDB("mgod1") + userModelTenant2 := s.getModelForDB("mgod2") + + userDoc := transactionTestUser{Name: "Gopher", EmailID: "gopher@mgod.com"} + + p, err := mgod.WithTransaction(context.Background(), func(sc mongo.SessionContext) (interface{}, error) { + _, err := userModelTenant1.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } + _, err = userModelTenant2.InsertOne(sc, userDoc) + if err != nil { + return nil, err + } + + userCount1, err := userModelTenant1.CountDocuments(sc, bson.M{}) + if err != nil { + return nil, err + } + userCount2, err := userModelTenant2.CountDocuments(sc, bson.M{}) + if err != nil { + return nil, err + } + + _, err = userModelTenant1.DeleteOne(sc, bson.M{}) + if err != nil { + return nil, err + } + _, err = userModelTenant2.DeleteOne(sc, bson.M{}) + if err != nil { + return nil, err + } + + return fmt.Sprintf("%d%d", userCount1, userCount2), nil + }) + + userCountStr, ok := p.(string) + + s.NoError(err) + s.True(ok) + s.Equal(userCountStr, "11") +} From 1b3208064bfedf48d5ab8d3d65f53c820163af51 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 12:29:26 +0530 Subject: [PATCH 4/6] doc: fix mgod usage example in README --- README.md | 10 ++-------- docs/basic_usage.md | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 48a29ea..dae615a 100644 --- a/README.md +++ b/README.md @@ -87,12 +87,8 @@ model := User{} dbName := "mgoddb" collection := "users" -schemaOpts := schemaopt.SchemaOptions{ - Timestamps: true, -} - -opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) -userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) +opts := mgod.NewEntityMongoModelOptions(dbName, collection, nil) +userModel, _ := mgod.NewEntityMongoModel(model, *opts) ``` Use the entity ODM to perform CRUD operations with ease. @@ -115,8 +111,6 @@ user, _ := userModel.InsertOne(context.TODO(), userDoc) "name": "Gopher", "emailId": "gopher@mgod.com", "joinedOn": ISODate("2023-12-01T11:32:19.290Z"), - "createdAt": ISODate("2023-12-01T11:32:19.290Z"), - "updatedAt": ISODate("2023-12-01T11:32:19.290Z"), "__v": 0 } ``` diff --git a/docs/basic_usage.md b/docs/basic_usage.md index b081489..cca3861 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -64,7 +64,7 @@ schemaOpts := schemaopt.SchemaOptions{ } opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) -userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts) +userModel, _ := mgod.NewEntityMongoModel(model, *opts) ``` Use the entity ODM to perform CRUD operations with ease. From 9439185eb52b46e79d94e2cc89785648b29588e8 Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Wed, 20 Dec 2023 13:40:16 +0530 Subject: [PATCH 5/6] doc: multi tenancy advanced guide --- README.md | 2 +- docs/README.md | 1 + docs/multi_tenancy.md | 44 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 docs/multi_tenancy.md diff --git a/README.md b/README.md index dae615a..5b2230b 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ user, _ := userModel.InsertOne(context.TODO(), userDoc) ``` **Output:** -```json +```js { "_id": ObjectId("65697705d4cbed00e8aba717"), "name": "Gopher", diff --git a/docs/README.md b/docs/README.md index c06c3a3..3d05b44 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,5 +18,6 @@ This directory contains user-facing documentation. For those who wish to underst * [Meta fields](./meta_fields.md) ### Advanced Guide +* [Multi Tenancy](./multi_tenancy.md) * [Unions](./union_types.md) * [Transactions](./transactions.md) diff --git a/docs/multi_tenancy.md b/docs/multi_tenancy.md new file mode 100644 index 0000000..058b560 --- /dev/null +++ b/docs/multi_tenancy.md @@ -0,0 +1,44 @@ +--- +title: Multi Tenancy +--- + +`mgod` comes with the built-in support for multi-tenancy, enabling the use of a single Go struct with multiple databases. This feature allows creation of multiple `EntityMongoModel` of the same Go struct to be attached to different databases while using the same underlying MongoDB client connection. + +## Usage + +Create separate `EntityMongoModel` for different tenants using same Go struct and corresponding databases. + +```go +type User struct { + Name string + EmailID string `bson:"emailId"` + Amount float32 +} +collection := "users" + +tenant1DB := "tenant1" +tenant2DB := "tenant2" + +tenant1Model, _ := mgod.NewEntityMongoModelOptions(tenant1DB, collection, nil) +tenant2Model, _ := mgod.NewEntityMongoModelOptions(tenant2DB, collection, nil) +``` + +These models can now be used simultaneously inside the same service logic as well as in a transaction operation. + +```go +amount := 10000 + +tenant1Model.UpdateMany(context.TODO(), bson.M{"name": "Gopher Tenant 1"}, bson.M{"$inc": {"amount": -amount}}) +tenant2Model.UpdateMany(context.TODO(), bson.M{"name": "Gopher Tenant 2"}, bson.M{"$inc": {"amount": amount}}) +``` + +:::note +The `EntityMongoModel` is always bound to the specified database at the time of its declaration and, as such, cannot be used to perform operations across multiple databases. +::: + +```go +amount := 10000 + +result, _ := tenant1Model.FindOne(context.TODO(), bson.M{"name": "Gopher Tenant 2"}) +// result will be value in this case +``` From 59928b646b7689dde760c3a1abdcbfcec7dc6b6a Mon Sep 17 00:00:00 2001 From: harsh-2711 Date: Thu, 21 Dec 2023 11:16:16 +0530 Subject: [PATCH 6/6] fix: add locking in connection cache --- connection.go | 2 +- connection_cache.go | 45 +++++++++++++++++++++++++++++++++----- docs/basic_usage.md | 2 +- docs/multi_tenancy.md | 2 -- entity_mongo_model_test.go | 5 ++++- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/connection.go b/connection.go index 6b0ac10..a2435f0 100644 --- a/connection.go +++ b/connection.go @@ -18,7 +18,7 @@ type ConnectionConfig struct { } // SetDefaultClient sets the default MongoDB client to be used by the package. -func SetDefaultClient(client *mongo.Client, dbName string) { +func SetDefaultClient(client *mongo.Client) { mClient = client } diff --git a/connection_cache.go b/connection_cache.go index cc8ed96..8c9faeb 100644 --- a/connection_cache.go +++ b/connection_cache.go @@ -1,21 +1,54 @@ package mgod -import "go.mongodb.org/mongo-driver/mongo" +import ( + "sync" + + "go.mongodb.org/mongo-driver/mongo" +) // dbConnCache is a cache of MongoDB database connections. -var dbConnCache map[string]*mongo.Database +var dbConnCache *connectionCache func init() { - dbConnCache = make(map[string]*mongo.Database) + dbConnCache = newConnectionCache() +} + +// connectionCache is a thread safe construct to cache MongoDB database connections. +type connectionCache struct { + cache map[string]*mongo.Database + mux sync.RWMutex +} + +func newConnectionCache() *connectionCache { + return &connectionCache{ + cache: map[string]*mongo.Database{}, + } +} + +func (c *connectionCache) Get(dbName string) *mongo.Database { + c.mux.RLock() + defer c.mux.RUnlock() + + return c.cache[dbName] +} + +func (c *connectionCache) Set(dbName string, db *mongo.Database) { + c.mux.Lock() + defer c.mux.Unlock() + + c.cache[dbName] = db } // getDBConn returns a MongoDB database connection from the cache. // If the connection is not present in the cache, it creates a new connection and adds it to the cache (Write-through policy). func getDBConn(dbName string) *mongo.Database { + dbConn := dbConnCache.Get(dbName) + // Initialize the cache entry if it is not present. - if dbConnCache[dbName] == nil { - dbConnCache[dbName] = mClient.Database(dbName) + if dbConn == nil { + dbConn = mClient.Database(dbName) + dbConnCache.Set(dbName, dbConn) } - return dbConnCache[dbName] + return dbConn } diff --git a/docs/basic_usage.md b/docs/basic_usage.md index cca3861..0d96a28 100644 --- a/docs/basic_usage.md +++ b/docs/basic_usage.md @@ -10,7 +10,7 @@ import "github.com/Lyearn/mgod" func init() { // client is the MongoDB client obtained using Go Mongo Driver's Connect method. - mgod.SetDefaultClient(client, dbName) + mgod.SetDefaultClient(client) } ``` diff --git a/docs/multi_tenancy.md b/docs/multi_tenancy.md index 058b560..af41cc0 100644 --- a/docs/multi_tenancy.md +++ b/docs/multi_tenancy.md @@ -37,8 +37,6 @@ The `EntityMongoModel` is always bound to the specified database at the time of ::: ```go -amount := 10000 - result, _ := tenant1Model.FindOne(context.TODO(), bson.M{"name": "Gopher Tenant 2"}) // result will be value in this case ``` diff --git a/entity_mongo_model_test.go b/entity_mongo_model_test.go index f0bf1bc..f394324 100644 --- a/entity_mongo_model_test.go +++ b/entity_mongo_model_test.go @@ -86,8 +86,11 @@ func (s *EntityMongoModelSuite) setupData() { } func (s *EntityMongoModelSuite) getModel() mgod.EntityMongoModel[testEntity] { + dbName := "mgoddb" + collection := "entityMongoModel" schemaOpts := schemaopt.SchemaOptions{Timestamps: true} - opts := mgod.NewEntityMongoModelOptions("mgoddb", "entityMongoModel", &schemaOpts) + + opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts) model, err := mgod.NewEntityMongoModel(testEntity{}, *opts) if err != nil { s.T().Fatal(err)