a MongoDB ORM for golang that rocks
Go to file
2024-09-14 18:19:45 -04:00
.idea huzzah for documentation!!! 🎉🥳 2024-09-14 18:19:45 -04:00
.vscode add a new Append method to Model, handle misc errors, unexport some funcs + methods 2024-09-02 19:39:23 -04:00
.gitignore unignore go.sum, ignore go.work* files 2024-09-12 23:18:53 -04:00
document_internals.go remove tons of commented-out code, general cleanup 2024-09-14 15:47:17 -04:00
document_slice.go overhaul models to be more sensible by separating "instance" logic into a separate Document type 🪷 2024-09-14 02:09:38 -04:00
document.go correct Pull method to break out of inner loop if a match is found 2024-09-14 15:10:14 -04:00
errors.go externalize more panic/error strings 2024-09-14 15:20:17 -04:00
go.mod update go.mod 2024-09-05 14:05:51 -04:00
go.sum unignore go.sum, ignore go.work* files 2024-09-12 23:18:53 -04:00
godoc.html huzzah for documentation!!! 🎉🥳 2024-09-14 18:19:45 -04:00
gridfs.go unexport Model fields 2024-09-14 15:28:34 -04:00
idcounter.go externalize more panic/error strings 2024-09-14 15:20:17 -04:00
indexes.go add a new Append method to Model, handle misc errors, unexport some funcs + methods 2024-09-02 19:39:23 -04:00
model_test.go overhaul models to be more sensible by separating "instance" logic into a separate Document type 🪷 2024-09-14 02:09:38 -04:00
model.go huzzah for documentation!!! 🎉🥳 2024-09-14 18:19:45 -04:00
query.go remove tons of commented-out code, general cleanup 2024-09-14 15:47:17 -04:00
README.md huzzah for documentation!!! 🎉🥳 2024-09-14 18:19:45 -04:00
registry.go add Get method to model registry 2024-09-14 18:07:00 -04:00
testing.go remove tons of commented-out code, general cleanup 2024-09-14 15:47:17 -04:00
util.go remove tons of commented-out code, general cleanup 2024-09-14 15:47:17 -04:00

diamond

a golang ORM for mongodb that rocks 🎸~♬

usage

installation

run the following command in your terminal...

go get rockfic.com/orm

...and import the package at the top of your file(s) like so:

package tutorial

import "rockfic.com/orm"

Connect to the database

package tutorial

import "rockfic.com/orm"

func main() {
	orm.Connect("mongodb://127.0.0.1", "your_database")
}

this will create a connection and store it in the DB global variable. This global variable is used internally to interact with the underlying database.

if you need to why?, you may also access the mongoDB client directly via the orm.Client variable.

Create a Model

to create a new model, you need to define a struct like this:

package tutorial

import "rockfic.com/orm"

type User struct {
	orm.Document `bson:",inline" coll:"collection"`
  ID int64 `bson:"_id"`
	Username     string `bson:"username"`
	Email        string `bson:"email"`
	Friends      []User `bson:"friends"`
}

this on its own is useless. to actually do anything useful with it, you need to register this struct as a model:

package tutorial

import "rockfic.com/orm"

func main() {
	/* ~ snip ~ */
	orm.ModelRegistry.Model(User{})
}

you can also pass multiple arguments to orm.Model, so long as they embed the Document struct.

you can access the newly created model like this:

package tutorial

import "rockfic.com/orm"

func main() {
  /* ~ snip ~ */
  userModel := orm.ModelRegistry.Get("User")
}

Documents

any struct can be used as a document, so long as it embeds the Document struct. the Document struct is special, in that it turns any structs which embed it into IDocument implementors. Document should be embedded with a `bson:",inline" tag, otherwise you will end up with something like this in the database:

{
  "document": {
    "createdAt": ISODate('2001-09-11T05:37:18.742Z'),
    "updatedAt": ISODate('2001-09-11T05:37:18.742Z')
  },
  _id: 1,
  username: "foobar",
  email: "test@testi.ng",
  friends: []
}

a coll or collection tag is also required, to assign the model to a mongodb collection. this tag is only valid on the embedded Document field, and each document can only have one collection associated with it. obviously. i mean seriously, who'd want to store one thing in two places?

the recommended way to create a new document instance is via the orm.Create method:

package tutorial

import "rockfic.com/orm"

func main() {
  /* ~ snip ~ */
  user := orm.Create(User{}).(*User)
}

similarly, to create a slice of documents, call orm.CreateSlice:

package tutorial

import "rockfic.com/orm"

func main() {
  /* ~ snip ~ */
  users := orm.CreateSlice[User](User{})
}

lastly, let's implement the HasID interface on our document:

package tutorial

import "rockfic.com/orm"

func (u *User) GetId() any {
  return u.ID
}

func (u *User) SetId(id any) {
  u.ID = id.(int64)
}

but why do i need to implement this? :(

other ORMs rudely assume that you want your _ids to be mongodb ObjectIDs, which are fucking ugly, but also, more importantly, may not match your existing schema. by doing things this way, you can set your _id to be any of the following types:

  • int

  • int32

  • int64

  • uint

  • uint32

  • uint64

  • string

  • ObjectId

    (if you hate yourself that much)

finding/querying

Find, FindOne, FindByID, and FindPaged all return an *orm.Query object. to get the underlying value, simply call .Exec(), passing to it a pointer to the variable in which you wish to store the results.

Find example

package tutorial

import (
  "go.mongodb.org/mongo-driver/bson"
  "rockfic.com/orm"
)

func main() {
  /* ~ snip ~ */
  userModel := orm.ModelRegistry.Get("User")
  if userModel != nil {
    res := orm.CreateSlice(User{})
    jq, _ := userModel.Find(bson.M{"username": "foobar"})
    q.Exec(&res)
  }
}

saving/updating

to save a document, simply call the Save() method. if the document doesn't exist, the ORM will create it, otherwise it replaces the existing one.

referencing other documents

it's possible to store references to other documents/structs, or slices to other documents. given the User struct we defined above, which contains a slice (Friends) referencing other Users, we could change the Friends field in the User type to be populateable, like so:

type User struct {
	orm.Document `bson:",inline" coll:"collection"`
	ID           int64  `bson:"_id"`
	Username     string `bson:"username"`
	Email        string `bson:"email"`
-	Friends      []User `bson:"friends"`
+   Friends      []User `bson:"friends" ref:"User"`
}

assuming we've filled a user's Friends with a few friends...

package tutorial

import "rockfic.com/orm"

func main() {
  /* ~ snip ~ */
  user := orm.Create(User{}).(*User)
  for i := 0; i < 10; i++ {
    friend := orm.Create(User{ID: int64(i + 2)}).(*User)
    user.Friends = append(user.Friends, friend)
  }
}

...in the database, friends will be stored like this:

{
  // ~ snip ~ //
  friends: [
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    9,
    10,
    11
  ]
}

after retrieving a User from the database, you can load their Friends and their info by using the Populatefunction on the returned Query pointer, like in the following example:

package tutorial

import (
  "fmt"
  "go.mongodb.org/mongo-driver/bson"
  "rockfic.com/orm"
)

func main() {
  /* ~ snip ~ */
  query, err := userModel.FindOne(bson.M{"_id": 1})
  if err == nil {
    result := orm.Create(User{}).(*User)
    query.Populate("Friends").Exec(result)
    fmt.Printf("%+v", result)
  }
}

Models vs Documents: what's the difference?

a Model is a static type that contains information about a given Document-like type. a Document, on the other hand, is a full-fledged instances with its own set of methods. if you come from an object-oriented programming background, you can envision the relationship between a Model and Document something like this (using Java as an example):

public class Document {
    public void Pull(field String, elements ...Document) {
        // ...
    }
    public void Append(field String, elements ...Document) {
        // ...
    }
    public void Save() {
        //... 
    }
    public void Delete() {
        //...
    }

    // model methods //
    public static Document FindOne(HashMap<String, Object> query, MongoOptions options) {
        // ...
    }
    public static ArrayList<Document> FindAll(HashMap<String, Object> query, MongoOptions options) {
        // ...
    } 
}

Struct Tags

the following tags are recognized by the ORM:

ref

the syntax of the ref tag is:

`ref:"struct_name"`

where struct_name is the name of the struct you're referencing, as shown in Referencing other documents.

gridfs

you can load files stored in gridfs directly into your structs using this tag, which has the following syntax:

`gridfs:"bucket_name,file_fmt"

where:

  • bucket_name is the name of the gridfs bucket
  • file_fmt is a valid go template string that resolves to the unique file name. all exported methods and fields in the surrounding struct can be referenced in this template.

currently, the supported field types for the gridfs tag are:

  • string
  • []byte

idx/index

indexes can be defined using this tag on the field you want to create the index for. the syntax is as follows:

idx:{field || field1,field2,...},keyword1,keyword2,...

supported keywords are unique and sparse. background is technically allowed, but it's deprecated, and so will essentially be a no-op on mongodb 4.2+.

acknowledgements

Further reading

see the godocs for more details :)