8.1 KiB
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 _id
s 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 User
s, 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 Populate
function
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 bucketfile_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
- goonode/mogo, the project which largely inspired this one
Further reading
see the godocs for more details :)