15/03/2021 - GO, MONGODB
Bu örnekte, "blog" alanında "tags" (etiketler) ve "posts" (gönderiler) ile çalışacağız. Normalde etiketleri posta belgesinde bir dizi alanı olarak saklayabilirsiniz. Ancak bu, etiket verilerinin hızla tekrarlanmasına yol açar. Bundan kaçınmak için, etiket referanslarını posta belgesi içinde bir dizi alanı olarak saklayabilirsiniz. Gönderi başına etiket sayısı az ve sınırlı olduğu için, etiket referansını posta belgesinin içinde saklamak mümkündür. Klasik bir 1-n ilişkisidir. Aşağıdaki örneğe bakın.
use blog
db.createCollection("tags")
db.tags.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})
db.tags.createIndex({"name":1},{unique:true,name:"UQ_name"})
db.createCollection("posts")
db.posts.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})
[
{
"_id": {
"$oid": "604f463ed42b77e01e7f03db"
},
"uuid": "07131782-0090-40cf-b530-68e774669826",
"name": "tech",
"description": "Technology related stuff"
},
{
"_id": {
"$oid": "604f463ed42b77e01e7f03dc"
},
"uuid": "a1e57bc6-de13-432d-ad22-7a3f5ba07b80",
"name": "sport",
"description": "Any kind of sport"
},
{
"_id": {
"$oid": "604f463ed42b77e01e7f03dd"
},
"uuid": "4f489d03-e4c9-444a-ba09-076d0584c96e",
"name": "travel",
"description": "Travel or holiday related stuff"
}
]
[
{
"_id": {
"$oid": "604f4831601401a8de9dfe1a"
},
"uuid": "acda18b2-f5c9-4736-8201-d1dc68c59354",
"subject": "Tech in football",
"text": "VAR ruined football so far!",
"created_at": {
"$date": "2021-03-15T11:42:41.171Z"
},
"tags": [
"07131782-0090-40cf-b530-68e774669826",
"a1e57bc6-de13-432d-ad22-7a3f5ba07b80"
]
},
{
"_id": {
"$oid": "604f4891601401a8de9dfe1b"
},
"uuid": "368f4fba-b1d1-4c58-9682-74ceedd276e9",
"subject": "Covid on holiday",
"text": "Do not take your Covid on holiday with you :)",
"created_at": {
"$date": "2021-03-15T11:44:17.613Z"
},
"tags": [
"4f489d03-e4c9-444a-ba09-076d0584c96e"
]
},
{
"_id": {
"$oid": "604f494e601401a8de9dfe1c"
},
"uuid": "ba87a063-7379-4b13-b7a5-d986f2bed43c",
"subject": "Pasta or not to pasta?",
"text": "Let's talk about what pasta is easiest to make.",
"created_at": {
"$date": "2021-03-15T11:47:26.607Z"
},
"tags": null
}
]
package storage
import (
"context"
"time"
)
type PostStorer interface {
Insert(ctx context.Context, post PostWrite) error
Find(ctx context.Context, uuid string) (PostRead, error)
}
type PostWrite struct {
UUID string `bson:"uuid"`
Subject string `bson:"subject"`
Text string `bson:"text"`
CreatedAt time.Time `bson:"created_at"`
TagUUIDs []string `bson:"tags"`
}
type PostRead struct {
ID string `bson:"_id"`
UUID string `bson:"uuid"`
Subject string `bson:"subject"`
Text string `bson:"text"`
CreatedAt time.Time `bson:"created_at"`
Tags []Tag `bson:"tags"`
}
type Tag struct {
ID string `bson:"_id"`
UUID string `bson:"uuid"`
Name string `bson:"name"`
Description string `bson:"description"`
}
package mongodb
import (
"context"
"log"
"time"
"github.com/you/mongo/internal/pkg/domain"
"github.com/you/mongo/internal/pkg/storage"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
var _ storage.PostStorer = PostStorage{}
type PostStorage struct {
Database *mongo.Database
Timeout time.Duration
}
func (p PostStorage) Insert(ctx context.Context, post storage.PostWrite) error {
ctx, cancel := context.WithTimeout(ctx, p.Timeout)
defer cancel()
if _, err := p.Database.Collection("posts").InsertOne(ctx, post); err != nil {
log.Println(err)
if er, ok := err.(mongo.WriteException); ok && er.WriteErrors[0].Code == 11000 {
return domain.ErrConflict
}
return domain.ErrInternal
}
return nil
}
func (p PostStorage) Find(ctx context.Context, uuid string) (storage.PostRead, error) {
ctx, cancel := context.WithTimeout(ctx, p.Timeout)
defer cancel()
// Tags are not sorted here!
// qry := []bson.M{
// {
// "$match": bson.M{
// "uuid": uuid,
// },
// },
// {
// "$lookup": bson.M{
// "from": "tags", // Child collection to join
// "localField": "tags", // Parent collection reference holding field
// "foreignField": "uuid", // Child collection reference field
// "as": "tags", // Arbitrary field name to store result set
// },
// },
// }
// Tags are asc sorted here!
qry := []bson.M{
{
"$match": bson.M{
"uuid": uuid,
},
},
{
"$lookup": bson.M{
// Define the tags collection for the join.
"from": "tags",
// Specify the variable to use in the pipeline stage.
"let": bson.M{
"tags": "$tags",
},
"pipeline": []bson.M{
// Select only the relevant tags from the tags collection.
// Otherwise all the tags are selected.
{
"$match": bson.M{
"$expr": bson.M{
"$in": []interface{}{
"$uuid",
"$$tags",
},
},
},
},
// Sort tags by their name field in asc. -1 = desc
{
"$sort": bson.M{
"name": 1,
},
},
},
// Use tags as the field name to match struct field.
"as": "tags",
},
},
}
cur, err := p.Database.Collection("posts").Aggregate(ctx, qry)
if err != nil {
log.Println(err)
return storage.PostRead{}, domain.ErrInternal
}
var pos []storage.PostRead
if err := cur.All(context.Background(), &pos); err != nil {
log.Println(err)
return storage.PostRead{}, domain.ErrInternal
}
defer cur.Close(context.Background())
if err := cur.Err(); err != nil {
log.Println(err)
return storage.PostRead{}, domain.ErrInternal
}
if len(pos) == 0 {
return storage.PostRead{}, domain.ErrNotFound
}
return pos[0], nil
}
package post
import "time"
// Request
type Create struct {
Subject string `json:"subject"`
Text string `json:"text"`
Tags []string `json:"tags"`
}
// Response
type Response struct {
ID string `json:"id"`
UUID string `json:"uuid"`
Subject string `json:"subject"`
Text string `json:"text"`
CreatedAt time.Time `json:"created_at"`
Tags []Tag `json:"tags"`
}
type Tag struct {
ID string `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
}
Gördüğünüz gibi bu dosya çok şey yapıyor ve gerçekten uygun bir istek doğrulaması yok. İhtiyaçlarınıza göre yeniden düzenlemelisiniz.
package post
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/you/mongo/internal/pkg/domain"
"github.com/you/mongo/internal/pkg/storage"
"github.com/google/uuid"
"github.com/julienschmidt/httprouter"
)
type Controller struct {
Storage storage.PostStorer
}
// POST /api/v1/posts
func (c Controller) Create(w http.ResponseWriter, r *http.Request) {
var req Create
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
id := uuid.New().String()
err := c.Storage.Insert(r.Context(), storage.PostWrite{
UUID: id,
Subject: fmt.Sprintf("%s - %d", req.Subject, time.Now().UTC().Nanosecond()),
Text: fmt.Sprintf("%s - %d", req.Text, time.Now().UTC().Nanosecond()),
CreatedAt: time.Now(),
TagUUIDs: req.Tags,
})
if err != nil {
switch err {
case domain.ErrConflict:
w.WriteHeader(http.StatusConflict)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(id))
}
// GET /api/v1/posts/:uuid
func (c Controller) Find(w http.ResponseWriter, r *http.Request) {
id := httprouter.ParamsFromContext(r.Context()).ByName("uuid")
com, err := c.Storage.Find(r.Context(), id)
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}
res := Response{
ID: com.ID,
UUID: com.UUID,
Subject: com.Subject,
Text: com.Text,
CreatedAt: com.CreatedAt,
Tags: make([]Tag, len(com.Tags)),
}
for i, tag := range com.Tags {
res.Tags[i].ID = tag.ID
res.Tags[i].UUID = tag.UUID
res.Tags[i].Name = tag.Name
res.Tags[i].Description = tag.Description
}
body, err := json.Marshal(res)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(body)
}
curl --request GET 'http://localhost:3000/api/v1/posts/acda18b2-f5c9-4736-8201-d1dc68c59354'
{
"id": "604f4831601401a8de9dfe1a",
"uuid": "acda18b2-f5c9-4736-8201-d1dc68c59354",
"subject": "Tech in football",
"text": "VAR ruined football so far!",
"created_at": "2021-03-15T11:42:41.171Z",
"tags": [
{
"ID": "604f463ed42b77e01e7f03dc",
"uuid": "a1e57bc6-de13-432d-ad22-7a3f5ba07b80",
"name": "sport",
"description": "Any kind of sport"
},
{
"ID": "604f463ed42b77e01e7f03db",
"uuid": "07131782-0090-40cf-b530-68e774669826",
"name": "tech",
"description": "Technology related stuff"
}
]
}
curl --request POST 'http://localhost:3000/api/v1/posts' \
--header 'Content-Type: application/json' \
--data-raw '{
"subject": "Subject 1",
"text": "Text 1",
"tags": [
"07131782-0090-40cf-b530-68e774669826"
]
}'
49329796-9f04-44d1-99fb-49128bba7a9c