huzzah for documentation!!! 🎉🥳
This commit is contained in:
parent
53e23bb52a
commit
64b9d1c241
@ -1,5 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GitSharedSettings">
|
||||||
|
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="synchronizeBranchProtectionRules" value="false" />
|
||||||
|
</component>
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
365
README.md
Normal file
365
README.md
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
# 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 :)
|
1229
godoc.html
Normal file
1229
godoc.html
Normal file
File diff suppressed because one or more lines are too long
10
model.go
10
model.go
@ -10,7 +10,8 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model - "base" struct for all queryable models
|
// Model - type which contains "static" methods like
|
||||||
|
// Find, FindOne, etc.
|
||||||
type Model struct {
|
type Model struct {
|
||||||
Indexes map[string][]InternalIndex
|
Indexes map[string][]InternalIndex
|
||||||
Type reflect.Type
|
Type reflect.Type
|
||||||
@ -224,6 +225,9 @@ func Create(d any) any {
|
|||||||
return what
|
return what
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateSlice - convenience method which creates a new slice
|
||||||
|
// of type *T (where T is a type which embeds Document) and
|
||||||
|
// returns it
|
||||||
func CreateSlice[T any](d T) []*T {
|
func CreateSlice[T any](d T) []*T {
|
||||||
r, _, _ := createBase(d)
|
r, _, _ := createBase(d)
|
||||||
rtype := r.Type()
|
rtype := r.Type()
|
||||||
@ -232,7 +236,3 @@ func CreateSlice[T any](d T) []*T {
|
|||||||
newItem.Elem().Set(reflect.MakeSlice(rslice, 0, 0))
|
newItem.Elem().Set(reflect.MakeSlice(rslice, 0, 0))
|
||||||
return newItem.Elem().Interface().([]*T)
|
return newItem.Elem().Interface().([]*T)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) PrintMe() {
|
|
||||||
fmt.Printf("My name is %s !\n", nameOf(m))
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user