365 lines
8.1 KiB
Markdown
365 lines
8.1 KiB
Markdown
|
# 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 <sub><sub>~~why?~~</sub></sub>, 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.
|
||
|
<sub>obviously.</sub>
|
||
|
<sub><sub>i mean seriously, who'd want to store one thing in two places?</sub></sub>
|
||
|
|
||
|
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
|
||
|
|
||
|
<sub>(if you hate yourself that much)</sub>
|
||
|
|
||
|
## 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<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](#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 :)
|