# diamond ### a golang ORM for mongodb that rocks 🎸~♬ # usage ## installation run the following command in your terminal... ```shell go get rockfic.com/orm ``` ...and import the package at the top of your file(s) like so: ```go package tutorial import "rockfic.com/orm" ``` ## Connect to the database ```go 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: ```go 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: ```go 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: ```go 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: ```bson { "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: ```go package tutorial import "rockfic.com/orm" func main() { /* ~ snip ~ */ user := orm.Create(User{}).(*User) } ``` similarly, to create a slice of documents, call `orm.CreateSlice`: ```go package tutorial import "rockfic.com/orm" func main() { /* ~ snip ~ */ users := orm.CreateSlice[User](User{}) } ``` lastly, let's implement the `HasID` interface on our document: ```go 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 ```go 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: ```diff 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... ```go 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: ```json5 { // ~ 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: ```go 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): ```java 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 query, MongoOptions options) { // ... } public static ArrayList FindAll(HashMap 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](#referencing-other-documents). ### `gridfs` you can load files stored in [gridfs](https://www.mongodb.com/docs/manual/core/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](https://pkg.go.dev/text/template) 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](https://github.com/goonode/mogo), the project which largely inspired this one # Further reading see the [godocs]("https://git.tablet.sh/tablet/diamond-orm/src/branch/main/godoc.html") for more details :)