masalibの日記

システム開発、運用と猫の写真ブログです

golang の入門 mongodb 2

接続テストができたので実際に TODO を記録したいと思います

Todo の構造

データ構造は至ってシンプル

type Todo struct {
    ID        primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` //primitive.ObjectID `json:"id"`
    Completed bool               `json:"completed"`
    Body      string             `json:"body"`
}

primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"
という部分が前回やった TODO と違う部分になります

ID フィールドは、primitive.ObjectID 型を使用しています。
これは MongoDB のドキュメント ID を表すために使用される型で、ユニークな識別子を提供します。
このフィールドには、json:"id,omitempty"と bson:"_id,omitempty"というタグが付けられています。
これらのタグは、JSON および BSON(バイナリ形式の JSON、MongoDB で使用される)のシリアライズ/デシリアライズ時に、
フィールド名をどのように扱うかを指定します。
omitempty オプションは、フィールドがゼロ値(この場合は空の ObjectID)の場合に、そのフィールドを無視するよう指示します。

自動採番の仕組みが int の他の仕組みがあるならそちらを使いたいのですが調べたらこの方式だったのでこちらにしています

mongodb のクライアント接続

    //環境変数の読み込み
    envMap, err := godotenv.Read()
    if err != nil {
        log.Fatal("Error reading .env file: ", err)
    }
    // MongoDBのURI
    MONGODB_URI := envMap["MONGODB_URI"]

    // クライアントオプションを設定
    clientOptions := options.Client().ApplyURI(MONGODB_URI)

    // MongoDBクライアントを作成
    client, err := mongo.Connect(context.Background(), clientOptions)
    if err != nil {
        log.Fatal(err)
    }

    defer client.Disconnect(context.Background())

    err = client.Ping(context.Background(), nil)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to MongoDB!")

    //collection
    collection = client.Database("golang_db").Collection("todos")

最初に、godotenv.Read()関数を使用して.env ファイルから環境変数を読み込みます。
この操作が失敗した場合、log.Fatal を使用してエラーメッセージを出力し、プログラムを終了します。
環境変数から読み込んだ MongoDB の URI は、MONGODB_URI 変数に格納されます。

options.Client().ApplyURI(MONGODB_URI)を使用して MongoDB の接続オプションを設定します。
この設定は、MongoDB の URI を解析し、接続に必要なオプションをクライアントに適用します。
その後、mongo.Connect(context.Background(), clientOptions)を
呼び出して MongoDB クライアントを作成し、MongoDB サーバーへの接続を開始します。
接続に失敗した場合、エラーがログに記録され、プログラムが終了します。

接続が成功した後、client.Ping(context.Background(), nil)を使用して MongoDB サーバーに ping を送り、
接続が有効であることを確認します。この操作が失敗した場合も、
エラーがログに記録され、プログラムが終了します。

最後に、client.Database("golang_db").Collection("todos")を使用して、
golang_db データベースの todos コレクションへの参照を取得します。
この参照は、後続のデータベース操作で使用されます。

defer client.Disconnect(context.Background())

上記の部分は、関数が終了する直前に特定の処理を実行するために使用されます。
この場合、client.Disconnect(context.Background())が defer によって呼び出されることで、
関数の実行が終了する直前に MongoDB のクライアント接続が切断されることを保証します。
これにより、リソースのリークを防ぎ、アプリケーションの安定性を向上させることができます。

client.Disconnect メソッドは、MongoDB のクライアント接続を切断するために使用されます。
このメソッドには、操作の実行コンテキストを指定するための引数が必要です。
ここでは、context.Background()が使用されています。
context.Background()は、特にキャンセルやタイムアウトが必要ない場合に使用される、
空のコンテキストを生成します。これは、単純な接続切断のような、
追加のコンテキスト情報が不要な操作に適しています。

ルーティングの部分について

   // app server
    app := fiber.New()
    app.Get("/api/todos/:id", getTodos)//1件表示
    app.Get("/api/todos", getTodos)//全件
    app.Post("/api/todos", createTodo)
    app.Patch("/api/todos/:id", updateTodo)
    app.Delete("/api/todos/:id", deleteTodo)
    app.Listen(":" + envMap["APP_PORT"])

    log.Fatal(app.Listen(":" + envMap["APP_PORT"]))

前の記事とほぼ同じようなTODOですが

masalib.hatenablog.com

処理部分が関数になっている部分がちがいます

HTTP メソッド(GET、POST、PATCH、DELETE)に基づくルーティングを定義します。 これらのメソッドは、特定の URL パスにリクエストが来たときに実行される関数(ハンドラー)を指定します。 例えば、app.Get("/api/todos/:id", getTodos)は、/api/todos/:id への GET リクエストに対して getTodos 関数を呼び出します。 :id はパスパラメータで、リクエストされた TODO の ID を指定するために使用されます。 app.Listen(":" + envMap["APP_PORT"])は、環境変数 APP_PORT で指定されたポートでサーバーを起動し、リクエストの待受を開始します。

Todo の全件表示と1件表示

func getTodos(c *fiber.Ctx) error {
    var todos []Todo

    id := c.Params("id")

    if id == "" {
        cursor, err := collection.Find(context.Background(), bson.M{})
        if err != nil {
            return c.Status(500).JSON(fiber.Map{"error": err.Error()})
        }
        defer cursor.Close(context.Background())

        for cursor.Next(context.Background()) {
            var todo Todo
            if err := cursor.Decode(&todo); err != nil {
                return c.Status(500).JSON(fiber.Map{"error": err.Error()})
            }

            cursor.Decode(&todo)
            todos = append(todos, todo)
        }
        return c.Status(200).JSON(todos)
    } else {
        todo := new(Todo)
        oid, err := primitive.ObjectIDFromHex(id)
        if err != nil {
            return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
        }

        //IDをもとに検索
        err = collection.FindOne(context.Background(), bson.M{"_id": oid}).Decode(&todo)
        if err != nil {
            return c.Status(404).JSON(fiber.Map{"error": "Todo not found"})
        }
        return c.Status(200).JSON(todo)
    }
}

関数 getTodos は、HTTP リクエストのコンテキストを表す*fiber.Ctx オブジェクトを受け取り、エラーを返す可能性があります。
この関数は、リクエストから id パラメータを取得し、その値に基づいて異なる動作をします。
id が空の場合、つまり特定の Todo の ID が指定されていない場合、
関数は MongoDB の collection から全ての Todo を検索します。

Find メソッドは、検索条件に一致するドキュメントのカーソルを返します。
このカーソルを使用して、結果セットを反復処理し、各 Todo を todos スライスに追加します。

このプロセス中にエラーが発生した場合、関数は 500 ステータスコードとエラーメッセージを含む JSON レスポンスを返します。
全ての Todo が正常に取得された場合、関数は 200 ステータスコードと Todo リストを含む JSON レスポンスを返します。

一方、id が空でない場合、関数は指定された ID に基づいて特定の Todo を検索します。
primitive.ObjectIDFromHex メソッドを使用して、文字列形式の ID を MongoDB の ObjectID に変換します。
変換に失敗した場合、関数は 400 ステータスコードとエラーメッセージを含む JSON レスポンスを返します 。 ID が有効であれば、FindOne メソッドを使用して特定の Todo を検索し、
見つかった場合は 200 ステータスコードと Todo を含む JSON レスポンスを返します。
Todo が見つからない場合は、404 ステータスコードとエラーメッセージを含む JSON レスポンスを返します。

Todo の新規追加

// createTodo create a new todo
func createTodo(c *fiber.Ctx) error {
    // Parse body
    todo := new(Todo)
    if err := c.BodyParser(todo); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    if todo.Body == "" {
        return c.Status(400).JSON(fiber.Map{"error": "Body is required"})
    }
    // ID を生成
    todo.ID = primitive.NewObjectID()
    todo.Completed = false
    // Insert todo
    _, err := collection.InsertOne(context.Background(), todo)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    return c.Status(201).JSON(todo)
}

c.BodyParser(todo)を呼び出して、リクエストボディを Todo 構造体に解析し、格納します。
この操作が失敗した場合(例えば、リクエストボディが無効な JSON である場合)、
関数は HTTP ステータスコード 400(Bad Request)とエラーメッセージを含む JSON レスポンスを返します。

todo.Body が空文字列の場合、これは必須フィールドであるため、
同様に HTTP ステータスコード 400 とエラーメッセージを返します。

todo.ID = primitive.NewObjectID()

その後、todo.ID に新しいオブジェクト ID を割り当て、
todo.Completed を false に設定します。
これにより、新しい TODO アイテムが未完了状態で作成されます。

次に、collection.InsertOne(context.Background(), todo)を使用して、
todo オブジェクトを MongoDB コレクションに挿入します。
この操作が失敗した場合、HTTP ステータスコード 500(Internal Server Error)とエラーメッセージを含む JSON レスポンスを返します。

最後に、操作が成功した場合、作成された TODO アイテムを含む
JSON レスポンスと HTTP ステータスコード 201(Created)を返します。これにより、クライアントは新しい TODO アイテムが正常に作成されたことを確認できます。

Todo の更新

func updateTodo(c *fiber.Ctx) error {
    id := c.Params("id")
    fmt.Println("id:", id)
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
    }

    todo := new(Todo)
    if err := c.BodyParser(todo); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }

    fmt.Println("1todo.Body:", todo.Body)

    err = collection.FindOne(context.Background(), bson.M{"_id": oid}).Err()
    if err == mongo.ErrNoDocuments {
        // ドキュメントが存在しない場合の処理
        return c.Status(404).JSON(fiber.Map{"error": "Todo not found"})
    } else if err != nil {
        // その他のエラーが発生した場合の処理
        return c.Status(500).JSON(fiber.Map{"error": "Internal server error"})
    }

    filter := bson.M{"_id": oid}
    update := bson.M{"$set": bson.M{"completed": todo.Completed, "body": todo.Body}}
    _, err = collection.UpdateOne(context.Background(), filter, update)
    fmt.Println("todo.Body:", todo.Body)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }

    return c.Status(200).JSON(fiber.Map{"status": "ok"})
}

この updateTodo 関数は、Fiber フレームワークを使用して MongoDB 内の
特定の Todo ドキュメントを更新するためのものです。
まず、リクエストから id パラメータを取得し、それを使って MongoDB の ObjectID に変換します。
この ID が無効である場合、400 ステータスコードとエラーメッセージを返します。

次に、リクエストボディから Todo オブジェクトを解析します。解析に失敗した場合も、
400 ステータスコードとエラーメッセージを返します。
その後、指定された ID を持つドキュメントが MongoDB に存在するかどうかを確認します。
ドキュメントが見つからない場合は 404 ステータスコードを、その他のエラーが発生した場合は 500 ステータスコードを返します。

ドキュメントが存在することが確認できたら、completed と body フィールドを更新します。
更新操作が成功すると、200 ステータスコードとステータスメッセージを含む JSON レスポンスを返します。
更新中にエラーが発生した場合は、500 ステータスコードとエラーメッセージを返します。

Todo の削除

// deleteTodo delete a todo
func deleteTodo(c *fiber.Ctx) error {
    // get id from url
    id := c.Params("id")

    // convert id to object id
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
    }
    // delete todo
    _, err = collection.DeleteOne(context.Background(), bson.M{"_id": oid})
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    return c.SendStatus(204)
}

このdeleteTodo関数は、Fiberフレームワークを使用して、
MongoDBのコレクションから特定のTODOアイテムを削除するためのものです。
まず、URLからTODOアイテムのIDを取得します。
このIDは、MongoDBのドキュメントを一意に識別するために使用されるObjectIDに変換する必要があります。

primitive.ObjectIDFromHex関数は、16進数の文字列をObjectIDに変換しますが、
この変換が失敗した場合(例えば、IDが16進数の形式ではない場合)、関数は400ステータスコードとエラーメッセージを返します。

IDの変換に成功した場合、collection.DeleteOneメソッドを使用して、
そのIDに対応するドキュメントを削除します。
この操作はcontext.Background()を使用して実行され、
これは特定のデッドラインやキャンセルシグナルがないデフォルトのコンテキストです。
削除操作が成功した場合、関数は204ステータスコード(No Content)を返して操作が成功したことを示します。
しかし、何らかの理由で削除操作が失敗した場合(例えば、データベース接続に問題がある場合)、
関数は500ステータスコードとエラーメッセージを返します。

全体のソース

.env は以下のような設定です。参考にする場合は修正してください

MONGODB_URI=mongodb+srv://masalib:password@cluster01.kliul2m.mongodb.net/golang_db?retryWrites=true&w=majority&appName=Cluster01
APP_PORT=5000

全体ソース(クリックすると展開されます)

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/gofiber/fiber/v2"
    "github.com/joho/godotenv"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type Todo struct {
    ID        primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"` //primitive.ObjectID `json:"id"`
    Completed bool               `json:"completed"`
    Body      string             `json:"body"`
}

var collection *mongo.Collection

func main() {
    fmt.Println("start main")
    mongodbtest()
}
func mongodbtest() {
    // .envファイル(環境変数)を読み込む
    envMap, err := godotenv.Read()
    if err != nil {
        log.Fatal("Error reading .env file: ", err)
    }
    // MongoDBのURI
    MONGODB_URI := envMap["MONGODB_URI"]

    // クライアントオプションを設定
    clientOptions := options.Client().ApplyURI(MONGODB_URI)

    // MongoDBクライアントを作成
    //client, err := mongo.NewClient(clientOptions)
    client, err := mongo.Connect(context.Background(), clientOptions)
    if err != nil {
        log.Fatal(err)
    }

    defer client.Disconnect(context.Background())

    err = client.Ping(context.Background(), nil)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Connected to MongoDB!")

    //collection
    collection = client.Database("golang_db").Collection("todos")

    // app server
    app := fiber.New()
    app.Get("/api/todos/:id", getTodos)
    app.Get("/api/todos", getTodos)
    app.Post("/api/todos", createTodo)
    app.Patch("/api/todos/:id", updateTodo)
    app.Delete("/api/todos/:id", deleteTodo)
    log.Fatal(app.Listen(":" + envMap["APP_PORT"]))
}

// getTodos get all todos
func getTodos(c *fiber.Ctx) error {
    var todos []Todo
    // get id from url
    id := c.Params("id")

    // find todo by id
    if id == "" {
        // get all todos
        cursor, err := collection.Find(context.Background(), bson.M{})
        if err != nil {
            return c.Status(500).JSON(fiber.Map{"error": err.Error()})
        }
        defer cursor.Close(context.Background())

        // loop through the cursor and decode each item into a Todo
        for cursor.Next(context.Background()) {
            var todo Todo
            if err := cursor.Decode(&todo); err != nil {
                return c.Status(500).JSON(fiber.Map{"error": err.Error()})
            }

            cursor.Decode(&todo)
            todos = append(todos, todo)
        }
        return c.Status(200).JSON(todos)
    } else {
        // get todo by id
        todo := new(Todo)
        oid, err := primitive.ObjectIDFromHex(id)
        if err != nil {
            return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
        }

        //IDをもとに検索
        err = collection.FindOne(context.Background(), bson.M{"_id": oid}).Decode(&todo)
        if err != nil {
            return c.Status(404).JSON(fiber.Map{"error": "Todo not found"})
        }
        return c.Status(200).JSON(todo)
    }
}

// createTodo create a new todo
func createTodo(c *fiber.Ctx) error {
    // Parse body
    todo := new(Todo)
    if err := c.BodyParser(todo); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }
    if todo.Body == "" {
        return c.Status(400).JSON(fiber.Map{"error": "Body is required"})
    }
    // ID を生成
    todo.ID = primitive.NewObjectID()
    todo.Completed = false
    // Insert todo
    _, err := collection.InsertOne(context.Background(), todo)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    return c.Status(201).JSON(todo)
}
func updateTodo(c *fiber.Ctx) error {
    id := c.Params("id")
    fmt.Println("id:", id)
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
    }

    todo := new(Todo)
    if err := c.BodyParser(todo); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": err.Error()})
    }

    fmt.Println("1todo.Body:", todo.Body)

    err = collection.FindOne(context.Background(), bson.M{"_id": oid}).Err()
    if err == mongo.ErrNoDocuments {
        // ドキュメントが存在しない場合の処理
        return c.Status(404).JSON(fiber.Map{"error": "Todo not found"})
    } else if err != nil {
        // その他のエラーが発生した場合の処理
        return c.Status(500).JSON(fiber.Map{"error": "Internal server error"})
    }

    //ヒットしたドキュメントをtodoの変数にいれる場合
    //err = collection.FindOne(context.Background(), bson.M{"_id": oid}).Decode(&todo)
    //if err != nil {
    // return c.Status(404).JSON(fiber.Map{"error": "Todo not found"})
    //}

    filter := bson.M{"_id": oid}
    update := bson.M{"$set": bson.M{"completed": todo.Completed, "body": todo.Body}}
    _, err = collection.UpdateOne(context.Background(), filter, update)
    fmt.Println("todo.Body:", todo.Body)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }

    return c.Status(200).JSON(fiber.Map{"status": "ok"})
}

// deleteTodo delete a todo
func deleteTodo(c *fiber.Ctx) error {
    // get id from url
    id := c.Params("id")

    // convert id to object id
    oid, err := primitive.ObjectIDFromHex(id)
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid ID"})
    }
    // delete todo
    _, err = collection.DeleteOne(context.Background(), bson.M{"_id": oid})
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    return c.SendStatus(204)
}