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, Golang'da yazılmış basit bir MongoDB CRUD örneğidir. Aynı zamanda en temel sayfalandırma özelliğine sahiptir. Unutmayın ki bazı dosyaların iyileştirilmesi gerekir. Bu yazıyı olabildiğince kısa tutmak için sabit değerler, tekrarlanmış kod vb. şeyler yaptım. İsteğinize göre yeniden düzenlemekten çekinmeyin.


Yapı


├── cmd
│   └── blog
│   └── main.go
├── docker
│   ├── docker-compose.yaml
│   └── mongodb
│   └── init.sh
└── internal
├── comment
│   ├── controller.go
│   ├── request.go
│   └── response.go
└── pkg
├── domain
│   └── error.go
└── storage
├── comment_storer.go
└── mongodb
├── comment.go
└── database.go

Dosyalar


main.go


package main

import (
"context"
"log"
"net/http"
"time"

"github.com/you/mongo/internal/comment"
"github.com/you/mongo/internal/pkg/storage/mongodb"
"github.com/julienschmidt/httprouter"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Initiate MongoDB instance.
mng, err := mongodb.NewDatabase(ctx, mongodb.DatabaseConfig{
Auth: "SCRAM-SHA-256",
Host: "localhost",
Port: "27017",
User: "inanzzz",
Pass: "1234567",
Name: "blog",
})
if err != nil {
log.Fatalln(err)
}
defer mng.Client().Disconnect(ctx)

// Initiate storage.
comStrg := mongodb.CommentStorage{
Database: mng,
Timeout: time.Second * 5,
}

// Initiate API.
com := comment.Controller{
Storage: comStrg,
}

// Initiate HTTP router.
rtr := httprouter.New()
rtr.HandlerFunc("POST", "/api/v1/comments", com.Create)
rtr.HandlerFunc("GET", "/api/v1/comments/:uuid", com.Find)
rtr.HandlerFunc("DELETE", "/api/v1/comments/:uuid", com.Delete)
rtr.HandlerFunc("PATCH", "/api/v1/comments/:uuid", com.Update)
rtr.HandlerFunc("GET", "/api/v1/comments", com.List)

// Initiate HTTP server.
log.Fatalln((&http.Server{Addr: ":3000", Handler: rtr}).ListenAndServe())
}

docker-compose.yaml


version: "3.4"

services:

blog-mongodb:
container_name: "blog-mongodb"
image: "mongo:4.0"
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: "root"
MONGO_INITDB_ROOT_PASSWORD: "root"
MONGO_NAME: "blog"
MONGO_USER: "inanzzz"
MONGO_PASS: "1234567"
MONGO_AUTH: "SCRAM-SHA-256"
volumes:
- "./mongodb:/docker-entrypoint-initdb.d"

init.sh


#!/bin/sh

# Create a custom user with a password, role and auth mechanism. This user will
# be used by the application for MongoDB connection.
mongo \
-u ${MONGO_INITDB_ROOT_USERNAME} \
-p ${MONGO_INITDB_ROOT_PASSWORD} \
--authenticationDatabase admin ${MONGO_NAME} \
<<-EOJS
db.createUser({
user: "${MONGO_USER}",
pwd: "${MONGO_PASS}",
roles: [{
role: "readWrite",
db: "${MONGO_NAME}"
}],
mechanisms: ["${MONGO_AUTH}"],
})
EOJS

# Prepare database.
mongo \
-u ${MONGO_INITDB_ROOT_USERNAME} \
-p ${MONGO_INITDB_ROOT_PASSWORD} \
--authenticationDatabase admin ${MONGO_NAME} \
<<-EOJS
use ${MONGO_NAME}
db.createCollection("comments")
db.comments.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})
EOJS

controller.go


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 comment

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"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.CommentStorer
}

// POST /api/v1/comments
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.Comment{
UUID: id,
Text: fmt.Sprintf("%s - %d", req.Text, time.Now().UTC().Nanosecond()),
CreatedAt: time.Now(),
})
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/comments/: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{
UUID: com.UUID,
Text: com.Text,
CreatedAt: com.CreatedAt,
}

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

// DELETE /api/v1/comments/:uuid
func (c Controller) Delete(w http.ResponseWriter, r *http.Request) {
id := httprouter.ParamsFromContext(r.Context()).ByName("uuid")

err := c.Storage.Delete(r.Context(), id)
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

w.WriteHeader(http.StatusNoContent)
}

// PATCH /api/v1/comments/:uuid
func (c Controller) Update(w http.ResponseWriter, r *http.Request) {
var req Update
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

err := c.Storage.Update(r.Context(), storage.Comment{
UUID: httprouter.ParamsFromContext(r.Context()).ByName("uuid"),
Text: req.Text,
})
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

w.WriteHeader(http.StatusNoContent)
}

// GET /api/v1/comments?page=1&limit=10&sort=-created_at
func (c Controller) List(w http.ResponseWriter, r *http.Request) {
page, err := strconv.Atoi(r.URL.Query().Get("page"))
if err != nil || page < 1 {
page = 1
}
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil || limit < 1 {
limit = 1
}

coms, err := c.Storage.List(r.Context(), page, limit)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

res := make([]Response, len(coms))
for i, com := range coms {
res[i] = Response{
UUID: com.UUID,
Text: com.Text,
CreatedAt: com.CreatedAt,
}
}

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

request.go


package comment

type Create struct {
Text string `json:"text"`
}

type Update struct {
Text string `json:"text"`
}

response.go


package comment

import "time"

type Response struct {
UUID string `json:"uuid"`
Text string `json:"text"`
CreatedAt time.Time `json:"created_at"`
}

error.go


package domain

import "errors"

var (
ErrInternal = errors.New("internal")
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
)

comment_storer.go


package storage

import (
"context"
"time"
)

type CommentStorer interface {
Insert(ctx context.Context, comment Comment) error
Find(ctx context.Context, uuid string) (Comment, error)
Delete(ctx context.Context, uuid string) error
Update(ctx context.Context, comment Comment) error
List(ctx context.Context, page, limit int) ([]*Comment, error)
}

type Comment struct {
UUID string `bson:"uuid"`
Text string `bson:"text"`
CreatedAt time.Time `bson:"created_at"`
}

comment.go


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"
"go.mongodb.org/mongo-driver/mongo/options"
)

var _ storage.CommentStorer = CommentStorage{}

type CommentStorage struct {
Database *mongo.Database
Timeout time.Duration
}

func (c CommentStorage) Insert(ctx context.Context, comment storage.Comment) error {
ctx, cancel := context.WithTimeout(ctx, c.Timeout)
defer cancel()

if _, err := c.Database.Collection("comments").InsertOne(ctx, comment); 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 (c CommentStorage) Find(ctx context.Context, uuid string) (storage.Comment, error) {
ctx, cancel := context.WithTimeout(ctx, c.Timeout)
defer cancel()

var com storage.Comment

qry := bson.M{"uuid": uuid}

err := c.Database.Collection("comments").FindOne(ctx, qry).Decode(&com)
if err != nil {
log.Println(err)

if err == mongo.ErrNoDocuments {
return storage.Comment{}, domain.ErrNotFound
}

return storage.Comment{}, domain.ErrInternal
}

return com, nil
}

func (c CommentStorage) Delete(ctx context.Context, uuid string) error {
ctx, cancel := context.WithTimeout(ctx, c.Timeout)
defer cancel()

qry := bson.M{"uuid": uuid}

res, err := c.Database.Collection("comments").DeleteOne(ctx, qry)
if err != nil {
log.Println(err)

return domain.ErrInternal
}
if res.DeletedCount == 0 {
return domain.ErrNotFound
}

return nil
}

func (c CommentStorage) Update(ctx context.Context, comment storage.Comment) error {
ctx, cancel := context.WithTimeout(ctx, c.Timeout)
defer cancel()

qry := bson.M{"uuid": comment.UUID}
upd := bson.M{"$set": bson.M{"text": comment.Text}}

// If required, this replaces the whole record.
// res, err := c.Database.Collection("comments").ReplaceOne(ctx, qry, comment)
res, err := c.Database.Collection("comments").UpdateOne(ctx, qry, upd)
if err != nil {
log.Println(err)

return domain.ErrInternal
}
if res.MatchedCount == 0 {
return domain.ErrNotFound
}

return nil
}

func (c CommentStorage) List(ctx context.Context, page, limit int) ([]*storage.Comment, error) {
ctx, cancel := context.WithTimeout(ctx, c.Timeout)
defer cancel()

var skip int
if page > 1 {
skip = (page - 1) * limit
}

qry := bson.M{}
opt := options.FindOptions{}
opt.SetSkip(int64(skip))
opt.SetLimit(int64(limit))
opt.SetSort(bson.M{"created_at": -1}) // This should come from user!

cur, err := c.Database.Collection("comments").Find(ctx, qry, &opt)
if err != nil {
log.Println(err)

return nil, domain.ErrInternal
}

var comments []*storage.Comment

for cur.Next(context.Background()) {
comment := &storage.Comment{}

err := cur.Decode(comment)
if err != nil {
log.Println(err)

return nil, domain.ErrInternal
}

comments = append(comments, comment)
}
defer cur.Close(context.Background())

if err := cur.Err(); err != nil {
log.Println(err)

return nil, domain.ErrInternal
}

return comments, nil
}

database.go


package mongodb

import (
"context"
"fmt"

"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)

type DatabaseConfig struct {
Auth string
Host string
Port string
User string
Pass string
Name string
}

func NewDatabase(ctx context.Context, cnf DatabaseConfig) (*mongo.Database, error) {
mon, err := mongo.Connect(ctx, options.Client().
SetAuth(options.Credential{
AuthMechanism: cnf.Auth,
AuthSource: cnf.Name,
Username: cnf.User,
Password: cnf.Pass,
}).
ApplyURI(fmt.Sprintf("mongodb://%s:%s", cnf.Host, cnf.Port)),
)
if err != nil {
return nil, err
}

return mon.Database(cnf.Name), nil
}

Test


# Create
curl --request POST 'http://localhost:3000/api/v1/comments' \
--header 'Content-Type: application/json' \
--data-raw '{"text": "Hello"}'

# Update
curl --request PATCH 'http://localhost:3000/api/v1/comments/11206d6f-8a77-44bc-9442-2ef6c7f98507' \
--header 'Content-Type: application/json' \
--data-raw '{"text": "Hello - 1"}'

# Find
curl --request GET 'http://localhost:3000/api/v1/comments/11206d6f-8a77-44bc-9442-2ef6c7f98507'

# Delete
curl --request DELETE 'http://localhost:3000/api/v1/comments/11206d6f-8a77-44bc-9442-2ef6c7f98507'

# List
curl --request GET 'http://localhost:3000/api/v1/comments?page=1&limit=4'