Skip to content

Commit

Permalink
feat: add wrapper function to support MongoDB transactions (#12)
Browse files Browse the repository at this point in the history
* feat: add wrapper function to support MongoDB transactions

* doc: mark transactions feature as done in future scope section

* fix: support multi tenancy

* doc: fix mgod usage example in README

* doc: multi tenancy advanced guide

* fix: add locking in connection cache
  • Loading branch information
harsh-2711 authored Dec 21, 2023
1 parent d05bfe7 commit 539402a
Show file tree
Hide file tree
Showing 16 changed files with 528 additions and 217 deletions.
4 changes: 2 additions & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
25 changes: 11 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,8 +43,8 @@ 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)
// client is the MongoDB client obtained using Go Mongo Driver's Connect method.
mgod.SetDefaultClient(client)
}
```

Expand All @@ -59,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)
}
```

Expand All @@ -84,12 +84,11 @@ import (
)

model := User{}
schemaOpts := schemaopt.SchemaOptions{
Collection: "users",
Timestamps: true,
}
dbName := "mgoddb"
collection := "users"

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.
Expand All @@ -106,14 +105,12 @@ user, _ := userModel.InsertOne(context.TODO(), userDoc)
```

**Output:**
```json
```js
{
"_id": ObjectId("65697705d4cbed00e8aba717"),
"name": "Gopher",
"emailId": "[email protected]",
"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
}
```
Expand Down Expand Up @@ -143,9 +140,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.
Expand Down
20 changes: 9 additions & 11 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,27 @@ import (
"go.mongodb.org/mongo-driver/mongo/options"
)

var dbConn *mongo.Database
var mClient *mongo.Client
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
// SetDefaultClient sets the default MongoDB client to be used by the package.
func SetDefaultClient(client *mongo.Client) {
mClient = client
}

// 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 {
// 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()
}

client, err := newClient(cfg, opts...)
mClient, err = newClient(cfg, opts...)
if err != nil {
return err
}
Expand All @@ -37,13 +37,11 @@ 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)

return nil
}

Expand Down
54 changes: 54 additions & 0 deletions connection_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package mgod

import (
"sync"

"go.mongodb.org/mongo-driver/mongo"
)

// dbConnCache is a cache of MongoDB database connections.
var dbConnCache *connectionCache

func init() {
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 dbConn == nil {
dbConn = mClient.Database(dbName)
dbConnCache.Set(dbName, dbConn)
}

return dbConn
}
4 changes: 3 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ This directory contains user-facing documentation. For those who wish to underst
* [Meta fields](./meta_fields.md)

### Advanced Guide
* [Unions](./union_types.md)
* [Multi Tenancy](./multi_tenancy.md)
* [Unions](./union_types.md)
* [Transactions](./transactions.md)
14 changes: 8 additions & 6 deletions docs/basic_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ 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)
// client is the MongoDB client obtained using Go Mongo Driver's Connect method.
mgod.SetDefaultClient(client)
}
```

Expand All @@ -26,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)
}
```

Expand Down Expand Up @@ -57,12 +56,15 @@ import (
)

model := User{}
dbName := "mgoddb"
collection := "users"

schemaOpts := schemaopt.SchemaOptions{
Collection: "users",
Timestamps: true,
}

userModel, _ := mgod.NewEntityMongoModel(model, schemaOpts)
opts := mgod.NewEntityMongoModelOptions(dbName, collection, &schemaOpts)
userModel, _ := mgod.NewEntityMongoModel(model, *opts)
```

Use the entity ODM to perform CRUD operations with ease.
Expand Down
7 changes: 3 additions & 4 deletions docs/meta_fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ It is the meta field that stores the timestamp of the document creation. This fi

```go
schemaOpts := schemaopt.SchemaOptions{
Collection: "users",
Timestamps: true,
}

userDoc := User{
Name: "Gopher",
EmailID: "[email protected]",
}

user, _ := userModel.InsertOne(context.TODO(), userDoc)
```

Expand All @@ -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,
}

Expand Down Expand Up @@ -98,14 +97,14 @@ It is the field that stores the version of the document. This field is automatic

```go
schemaOpts := schemaopt.SchemaOptions{
Collection: "users",
VersionKey: true
}

userDoc := User{
Name: "Gopher",
EmailID: "[email protected]",
}

user, _ := userModel.InsertOne(context.TODO(), userDoc)
```

Expand All @@ -124,14 +123,14 @@ If `VersionKey` is set to `false`.

```go
schemaOpts := schemaopt.SchemaOptions{
Collection: "users",
VersionKey: false
}

userDoc := User{
Name: "Gopher",
EmailID: "[email protected]",
}

user, _ := userModel.InsertOne(context.TODO(), userDoc)
```

Expand Down
42 changes: 42 additions & 0 deletions docs/multi_tenancy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
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
result, _ := tenant1Model.FindOne(context.TODO(), bson.M{"name": "Gopher Tenant 2"})
// result will be <nil> value in this case
```
Loading

0 comments on commit 539402a

Please sign in to comment.