Herkese merhaba!

Uzun yıllardır bol miktarda kişisel zaman ve enerji harcayarak bilgimizi hepinizle paylaşıyoruz. Ancak şu andan itibaren bu blogu çalışır durumda tutabilmek için yardımınıza ihtiyacımız var. Yapmanız gereken tek şey, sitedeki reklamlardan birine tıklamak olacaktır, aksi takdirde hosting vb. masraflar nedeniyle maalesef yayından kaldırılacaktır. Teşekkürler.

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.


Veritabanı içeriği


Hazırlık


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"})

Etiket verileri


[
{
"_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"
}
]

Gönderi verileri


[
{
"_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
}
]

Depo


Modeller


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"`
}

Depolayıcı


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
}

HTTP Yönlendirici


Modeller


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"`
}

Kontrolör


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)
}

Test


Bulma


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"
}
]
}

Yaratma


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