LoginSignup
26
18
お題は不問!Qiita Engineer Festa 2023で記事投稿!

ChatGPTと対話しながら、クリーンアーキテクチャに基づくTodo APIを実装してみた

Last updated at Posted at 2023-06-29

はじめに

エンジニア1年目のSakaguchiです。
この記事は、クリーンアーキテクチャの学習にChatGPTを活用してみた際の記録です。

ChatGPTにサンプルコードをもらい、コードを修正しながら実装してくので、長めの記事になってしまいました。
できるだけ短くなるよう、ChatGPTの回答は端折りつつ、コードの細かい解説はしないようにしていますので、ご了承ください。

とりあえずどんなコードを書いたか見たいという方は、下記リポジトリを参照ください。

使用技術

使用技術はこんな感じです。

  • 言語
    • Golang
  • RDBMS
    • Postgres(Docker)
  • ライブラリ
    • Echo
    • gorm

実装手順の確認

実装に入る前にChatGPTに実装手順を聞いてみます。

Go / Echo で、クリーンアーキテクチャを採用してTodoAPIを作成したいです。
まずは、手順を教えてください。
ChatGPTからの回答

クリーンアーキテクチャを使用してTodo APIを作成する手順を以下に示します。

ステップ 1: プロジェクトのセットアップ
  • 必要なツールと言語のインストール:Go言語とEchoフレームワークをインストールします。
  • プロジェクトのディレクトリ作成:プロジェクトのためのディレクトリを作成し、その中にソースコードを配置します。
ステップ 2: プロジェクトの構造設計
  • クリーンアーキテクチャのディレクトリ構造を作成:以下のディレクトリ構造を作成します。
    • cmd: アプリケーションのエントリーポイントを含むディレクトリ。
    • internal: アプリケーションの内部パッケージを含むディレクトリ。
      • domain: ビジネスロジックやエンティティモデルを含むディレクトリ。
      • usecase: アプリケーションのユースケースを実装するディレクトリ。
      • repository: データベースアクセスや永続化を担当するディレクトリ。
      • delivery: ユーザーからのリクエストを処理するディレクトリ。
  • 各ディレクトリ内に必要なファイルを作成:各ディレクトリ内に必要なGoソースファイルを作成します。
ステップ 3: ビジネスロジックの実装
  • domainディレクトリ内にエンティティモデルを定義:Todoアイテムを表すエンティティモデルを定義します。
  • usecaseディレクトリ内にユースケースを実装:Todoの作成、読み取り、更新、削除などのユースケースを実装します。
ステップ 4: データベースアクセスの実装
  • repositoryディレクトリ内にリポジトリを実装:データベースへのアクセスや永続化を担当するリポジトリを実装します。
ステップ 5: リクエスト処理の実装
  • deliveryディレクトリ内にハンドラを実装:HTTPリクエストを処理するハンドラを実装します。
ステップ 6: ルーティングの設定
  • メインのエントリーポイントであるcmdディレクトリ内のファイルで、Echoフレームワークのル

ーティングを設定します。

  • 各エンドポイントに対して、適切なハンドラ関数を割り当てます。
ステップ 7: テスト
  • ユニットテストと統合テストを実装し、各コンポーネントが期待どおりに動作することを確認します。

これらの手順に従って、クリーンアーキテクチャを使用してTodo APIを作成できます。ただし、実装の詳細や個別のコードについては、具体的な要件や好みに基づいて調整する必要があります。

以降は、ChatGPTの回答から得た手順に沿って進めていきますが、テストは省略しようと思います。
少し長いですがお付き合いください。

ステップ1. プロジェクトのセットアップ

適当な場所で、プロジェクト用のディレクトリを作成し、ディレクトリに移動します。
今回、ディレクトリ名はgo-todoにしました。

mkdir go-todo
cd go-todo

go.modファイルを作成します。

go mod init go-todo

go.modファイルができたら、Echoとgormをインストールします。

go get github.com/labstack/echo/v4
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

次に、docker-compose.ymlを作成し、Postgresのコンテナを用意します。

docker-compose.yml
version: '3.8'

services:
  dev-postgres:
    image: postgres:15.1-alpine
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: todo-user
      POSTGRES_PASSWORD: todo-password
      POSTGRES_DB: todos
    restart: always

docker-compose upコマンドを実行し、以下のようになればOKです。

[+] Running 2/1
 ✔ Network go-todo_default  Created                     0.0s 
 ✔ Container go-todo-db-1   Recreated                   0.1s    

最後にプロジェクトディレクトリ直下に.envファイルを作成しておいてください。

POSTGRES_USER=todo-user
POSTGRES_PASSWORD=todo-password
POSTGRES_DB=todos
POSTGRES_PORT=5432
POSTGRES_HOST=localhost

以上でセットアップは完了です。

ステップ2. ディレクトリ構成

ディレクトリ構成は、ChatGPTに提案された通りの構成にしようと思います。

go-todo/
├── cmd/
│   └── main.go
├── internal/
│   ├── domain/
│   │   └── todo.go
│   ├── usecase/
│   │   └── todo_usecase.go
│   ├── repository/
│   │   └── todo_repository.go
│   └── delivery/
│       └── todo_handler.go
├── go.mod
└── docker-compose.yml

以降のセクションから、実装を行なっていきます。
まずは、ChatGPTにサンプルコードをもらい、それを修正しながら実装を進めていこうと思います。

ステップ3. ビジネスロジックの実装

このステップでは、DomainとUsecaseの実装を行います。
まずはDomainから実装していきます。

Domain

domainディレクトリ内にエンティティモデルを定義:Todoアイテムを表すエンティティモデルを定義します。

まずは、ChatGPTにサンプルコードをもらいます。

domainのサンプルコードをください。

ChatGPTから提案されたコードは下記です。

// domain/todo.go

package domain

// TodoはTodoアイテムを表すエンティティです。
type Todo struct {
	ID     int
	Title  string
	Status string
}

// TodoRepositoryはTodoアイテムの永続化と取得を担当するインターフェースです。
type TodoRepository interface {
	Create(todo *Todo) error
	GetByID(id int) (*Todo, error)
	Update(todo *Todo) error
	Delete(id int) error
}

ChatGPTによると、ドメイン層では、ビジネスロジックを表すエンティティと、それに関連するリポジトリインターフェースを定義するようです。

上記のコードでは、Todoというエンティティモデルが定義されています。このモデルは、TodoアイテムのID、タイトル、ステータスを表します。
また、TodoRepositoryというインターフェースも定義されています。このインターフェースは、Todoアイテムの永続化(作成、更新、削除)と取得を行うメソッドを宣言しています。具体的なデータベース操作や永続化の実装は、後続の手順で行います。
このようにドメイン層では、ビジネスロジックを表すエンティティと、それに関連するリポジトリインターフェースを定義します。

今回は、リポジトリインターフェースはrepositoryディレクトリ内に定義したいので削除します。
また、エンティティモデルにはフィールドタグを追加しました。

最終的なコードは下記です。

domain/todo.go
package domain

import "time"

type Todo struct {
	ID        uint      `json:"id" gorm:"primaryKey"`
	Title     string    `json:"title" gorm:"not null"`
	Status    string    `json:"status"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

次はUsecaseを実装していきます。

Usecase

usecaseディレクトリ内にユースケースを実装:Todoの作成、読み取り、更新、削除などのユースケースを実装します。

まずはChatGPTにサンプルコードをもらいます。

usecaseのサンプルコードをください

ChatGPTから提案されたコードは下記です。

// usecase/todo.go

package usecase

import "your-package/domain"

// TodoUsecaseはTodoアイテムのユースケースを実装する構造体です。
type TodoUsecase struct {
	todoRepository domain.TodoRepository
}

// NewTodoUsecaseはTodoUsecaseのインスタンスを作成します。
func NewTodoUsecase(todoRepo domain.TodoRepository) *TodoUsecase {
	return &TodoUsecase{
		todoRepository: todoRepo,
	}
}

// CreateTodoは新しいTodoアイテムを作成します。
func (uc *TodoUsecase) CreateTodo(title string) error {
	todo := &domain.Todo{
		Title:  title,
		Status: "Pending",
	}
	err := uc.todoRepository.Create(todo)
	if err != nil {
		return err
	}
	// 他のビジネスロジックの操作や通知などを行うことができます
	return nil
}

// UpdateTodoは既存のTodoアイテムを更新します。
func (uc *TodoUsecase) UpdateTodo(todo *domain.Todo) error {
	err := uc.todoRepository.Update(todo)
	if err != nil {
		return err
	}
	// 他のビジネスロジックの操作や通知などを行うことができます
	return nil
}

// DeleteTodoは指定されたIDのTodoアイテムを削除します。
func (uc *TodoUsecase) DeleteTodo(id int) error {
	err := uc.todoRepository.Delete(id)
	if err != nil {
		return err
	}
	// 他のビジネスロジックの操作や通知などを行うことができます
	return nil
}

構造体の定義とTodoUsecaseのインスタンスを作成するためのファクトリメソッドが定義されており、Todoに関するユースケースを実装しているようです。
提案されたコードには、インターフェースが定義されていないため、このままではユースケースの呼び出し先がユースケースの実装に依存してしまいます。
ということでインターフェースの追加を行い、ユースケースの実装も少し修正を加えます。

最終的なコードは下記です。

usecase/todo_usecase.go
package usecase

import (
	"go-todo/internal/domain"
	"go-todo/internal/repository"
)

type TodoUsecase interface {
	GetAllTodos() ([]domain.Todo, error)
	GetTodoById(todoId uint) (domain.Todo, error)
	CreateTodo(todo domain.Todo) error
	UpdateTodo(todoId uint, todo domain.Todo) error
	DeleteTodo(todoId uint) error
}

type todoUsecaseImpl struct {
	tr repository.TodoRepository
}

func NewTodoUsecase(tr repository.TodoRepository) TodoUsecase {
	return &todoUsecaseImpl{tr}
}

func (tu *todoUsecaseImpl) GetAllTodos() ([]domain.Todo, error) {
	todos := []domain.Todo{}
	if err := tu.tr.GetAllTodos(&todos); err != nil {
		return nil, err
	}

	return todos, nil
}

func (tu *todoUsecaseImpl) GetTodoById(todoId uint) (domain.Todo, error) {
	todo := domain.Todo{}
	if err := tu.tr.GetTodoById(todoId, &todo); err != nil {
		return domain.Todo{}, err
	}

	return todo, nil
}

func (tu *todoUsecaseImpl) CreateTodo(todo domain.Todo) error {
	if err := tu.tr.CreateTodo(&todo); err != nil {
		return err
	}

	return nil
}

func (tu *todoUsecaseImpl) UpdateTodo(todoId uint, todo domain.Todo) error {
	if err := tu.tr.UpdateTodo(todoId, &todo); err != nil {
		return err
	}

	return nil
}

func (tu *todoUsecaseImpl) DeleteTodo(todoId uint) error {
	if err := tu.tr.DeleteTodo(todoId); err != nil {
		return err
	}
	return nil
}

ステップ4. データベースアクセスの実装

repositoryディレクトリ内にリポジトリを実装:データベースへのアクセスや永続化を担当するリポジトリを実装します。

まずは、ChatGPTにサンプルコードをもらいます。

repositoryのサンプルコードをください。

ChatGPTに提案されたコードは下記です。

// repository/todo_repository.go

package repository

import "your-package/domain"

// TodoRepositoryImplはTodoアイテムの永続化と取得を担当するリポジトリの実装です。
type TodoRepositoryImpl struct {
	// データベース接続などの必要なフィールドを追加します
}

// NewTodoRepositoryImplはTodoRepositoryImplのインスタンスを作成します。
func NewTodoRepositoryImpl() *TodoRepositoryImpl {
	// データベース接続などの初期化処理を行います
	return &TodoRepositoryImpl{}
}

// Createは新しいTodoアイテムを作成します。
func (repo *TodoRepositoryImpl) Create(todo *domain.Todo) error {
	// データベースにTodoアイテムを保存する処理を実装します
	return nil
}

// GetByIDは指定されたIDのTodoアイテムを取得します。
func (repo *TodoRepositoryImpl) GetByID(id int) (*domain.Todo, error) {
	// データベースから指定されたIDのTodoアイテムを取得する処理を実装します
	return nil, nil
}

// Updateは既存のTodoアイテムを更新します。
func (repo *TodoRepositoryImpl) Update(todo *domain.Todo) error {
	// データベースでTodoアイテムを更新する処理を実装します
	return nil
}

// Deleteは指定されたIDのTodoアイテムを削除します。
func (repo *TodoRepositoryImpl) Delete(id int) error {
	// データベースから指定されたIDのTodoアイテムを削除する処理を実装します
	return nil
}

ステップ3で定義したリポジトリインターフェースの実装と、リポジトリをインスタンスするためのファクトリメソッドを定義しています。

先ほど、リポジトリインターフェースは、ドメイン層から削除したので、リポジトリ層に追加していきます。
また、関数名もサンプルコードから修正したいと思います。

最終的なコードは下記です。

repository/todo_repository.go
package repository

import (
	"fmt"
	"go-todo/internal/domain"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type TodoRepository interface {
	GetAllTodos(todos *[]domain.Todo) error
	GetTodoById(todoId uint, todo *domain.Todo) error
	CreateTodo(todo *domain.Todo) error
	UpdateTodo(todoId uint, todo *domain.Todo) error
	DeleteTodo(todoId uint) error
}

type todoRepositoryImpl struct {
	db *gorm.DB
}

func NewTodoRepository(db *gorm.DB) TodoRepository {
	return &todoRepositoryImpl{db}
}

func (tr *todoRepositoryImpl) GetAllTodos(todos *[]domain.Todo) error {
	if err := tr.db.Find(&todos).Error; err != nil {
		return err
	}
	return nil
}

func (tr *todoRepositoryImpl) GetTodoById(todoId uint, todo *domain.Todo) error {
	if err := tr.db.First(todo, todoId).Error; err != nil {
		return err
	}
	return nil
}

func (tr *todoRepositoryImpl) CreateTodo(todo *domain.Todo) error {
	if err := tr.db.Create(todo).Error; err != nil {
		return err
	}
	return nil
}

func (tr *todoRepositoryImpl) UpdateTodo(todoId uint, todo *domain.Todo) error {
	result := tr.db.Model(todo).Clauses(clause.Returning{}).Where("id=?", todoId).Updates(map[string]interface{}{"title": todo.Title, "status": todo.Status})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected < 1 {
		return fmt.Errorf("object does not exist")
	}
	return nil
}

func (tr *todoRepositoryImpl) DeleteTodo(todoId uint) error {
	result := tr.db.Where("id=?", todoId).Delete(&domain.Todo{})
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected < 1 {
		return fmt.Errorf("object does not exist")
	}
	return nil
}

ステップ5. リクエスト処理の実装

deliveryディレクトリ内にハンドラを実装:HTTPリクエストを処理するハンドラを実装します。

まずはChatGPTにサンプルコードをもらいます。

handlerのサンプルコードをください。

提案されたコードは下記です。

// delivery/todo_handler.go

package delivery

import (
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
	"your-package/domain"
	"your-package/usecase"
)

// TodoHandlerはTodoアイテムのリクエスト処理を担当するハンドラです。
type TodoHandler struct {
	todoUsecase usecase.TodoUsecase
}

// NewTodoHandlerはTodoHandlerのインスタンスを作成します。
func NewTodoHandler(todoUsecase usecase.TodoUsecase) *TodoHandler {
	return &TodoHandler{
		todoUsecase: todoUsecase,
	}
}

// CreateTodoは新しいTodoアイテムを作成するためのハンドラ関数です。
func (h *TodoHandler) CreateTodo(c echo.Context) error {
	title := c.FormValue("title")

	err := h.todoUsecase.CreateTodo(title)
	if err != nil {
		// エラーレスポンスを返すなどの処理を行います
		return c.JSON(http.StatusInternalServerError, err.Error())
	}

	// 成功レスポンスを返すなどの処理を行います
	return c.NoContent(http.StatusCreated)
}

// UpdateTodoは既存のTodoアイテムを更新するためのハンドラ関数です。
func (h *TodoHandler) UpdateTodo(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		// パラメータの解析に失敗した場合の処理を行います
		return c.JSON(http.StatusBadRequest, "Invalid ID")
	}

	todo := &domain.Todo{
		ID:     id,
		Title:  c.FormValue("title"),
		Status: c.FormValue("status"),
	}

	err = h.todoUsecase.UpdateTodo(todo)
	if err != nil {
		// エラーレスポンスを返すなどの処理を行います
		return c.JSON(http.StatusInternalServerError, err.Error())
	}

	// 成功レスポンスを返すなどの処理を行います
	return c.NoContent(http.StatusOK)
}

// DeleteTodoは指定されたIDのTodoアイテムを削除するためのハンドラ関数です。
func (h *TodoHandler) DeleteTodo(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		// パラメータの解析に失敗した場合の処理を行います
		return c.JSON(http.StatusBadRequest, "Invalid ID")
	}

	err = h.todoUsecase.DeleteTodo(id)
	if err != nil {
		// エラーレスポンスを返すなどの処理を行います
		return c.JSON(http.StatusInternalServerError, err.Error())
	}

	// 成功レスポンスを返すなどの処理を行います
	return c.NoContent(http.StatusOK)
}

構造体とTodoHandlerのインスタンスを作成するためのファクトリメソッドが定義されており、ハンドラー関数を実装しているようです。
ユースケース同様、インターフェースが定義されていないので追加しつつ、細かい修正を行います。

最終的なコードは下記です。

delivery/todo_handler.go
package handlers

import (
	"go-todo/internal/domain"
	"go-todo/internal/usecase"
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
)

type TodoHandler interface {
	GetAllTodos(c echo.Context) error
	GetTodoById(c echo.Context) error
	CreateTodo(c echo.Context) error
	UpdateTodo(c echo.Context) error
	DeleteTodo(c echo.Context) error
}

type todoHandlerImpl struct {
	tu usecase.TodoUsecase
}

func NewTodoHandler(tu usecase.TodoUsecase) TodoHandler {
	return &todoHandlerImpl{tu}
}

func (th *todoHandlerImpl) GetAllTodos(c echo.Context) error {
	todos, err := th.tu.GetAllTodos()
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	return c.JSON(http.StatusOK, todos)
}

func (th *todoHandlerImpl) GetTodoById(c echo.Context) error {
	id := c.Param("todoId")
	todoId, _ := strconv.Atoi(id)
	todo, err := th.tu.GetTodoById(uint(todoId))
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	return c.JSON(http.StatusOK, todo)
}

func (th *todoHandlerImpl) CreateTodo(c echo.Context) error {
	todo := domain.Todo{}
	if err := c.Bind(&todo); err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}

	if err := th.tu.CreateTodo(todo); err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	return c.JSON(http.StatusCreated, todo)
}

func (th *todoHandlerImpl) UpdateTodo(c echo.Context) error {
	id := c.Param("todoId")
	todoId, _ := strconv.Atoi(id)

	todo := domain.Todo{}
	if err := c.Bind(&todo); err != nil {
		return c.JSON(http.StatusBadRequest, err.Error())
	}
	if err := th.tu.UpdateTodo(uint(todoId), todo); err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	return c.JSON(http.StatusOK, todo)
}

func (th *todoHandlerImpl) DeleteTodo(c echo.Context) error {
	id := c.Param("todoId")
	todoId, _ := strconv.Atoi(id)

	err := th.tu.DeleteTodo(uint(todoId))
	if err != nil {
		return c.JSON(http.StatusInternalServerError, err.Error())
	}
	return c.NoContent(http.StatusNoContent)
}

ステップ6. ルーティングの設定

メインのエントリーポイントであるcmdディレクトリ内のファイルで、Echoフレームワークのルーティングを設定します。
各エンドポイントに対して、適切なハンドラ関数を割り当てます。

まずはChatGPTにサンプルコードをもらいます。

ルーティングのサンプルコードをください。

提案されたコードは下記です。

// delivery/routes.go ⭐️

package delivery

import (
	"github.com/labstack/echo/v4"
	"your-package/usecase"
)

// SetupRoutesはアプリケーションのルーティングを設定します。
func SetupRoutes(e *echo.Echo, todoUsecase usecase.TodoUsecase) {
	todoHandler := NewTodoHandler(todoUsecase)

	// Todo関連のエンドポイントを設定します
	e.POST("/todos", todoHandler.CreateTodo)
	e.PUT("/todos/:id", todoHandler.UpdateTodo)
	e.DELETE("/todos/:id", todoHandler.DeleteTodo)
}

はじめにクリーンアーキテクチャの実装手順をChatGPTに確認した時には、cmdディレクトリ内のファイルにルーティングを定義するとのことでしたが、どうやらdeliveryディレクトリ内に定義するようです。(⭐️)
また、なぜかhandlerではなく、usecaseをimportしてるので修正します。

まず、delivery内にルーティングを定義するので、ディレクトリ構成を下記のように変更しました。

go-todo/
├── cmd/
│   └── main.go
├── internal/
│   ├── domain/
│   │   └── todo.go
│   ├── usecase/
│   │   └── todo_usecase.go
│   ├── repository/
│   │   └── todo_repository.go
│   └── delivery/
│              ├──  handlers ⭐️追加
│       │   └── todo_handler.go
│       └── routes   ⭐️追加
│           └── todo_router.go
├── go.mod
└── docker-compose.yml

handlerをimportするようにし、ルーティングも追加します。
最終的なコードは下記です。

delivery/routes/todo_router.go
package routes

import (
	"go-todo/internal/delivery/handlers"

	"github.com/labstack/echo/v4"
)

func NewTodoRouter(th handlers.TodoHandler) *echo.Echo {
	e := echo.New()
	t := e.Group("/todos")
	t.GET("", th.GetAllTodos)
	t.GET("/:todoId", th.GetTodoById)
	t.POST("", th.CreateTodo)
	t.PUT("/:todoId", th.UpdateTodo)
	t.DELETE("/:todoId", th.DeleteTodo)

	return e
}

最後にPostgresとの接続とマイグレーション用のコードを追加します。

Postgresとの接続 & マイグレーション

Postgresに接続する

まずはPostgresとの接続を行います。
ChatGPTによると、データベースとの接続に関連するコードは、リポジトリ層に記述することが一般的とのことです。

データベースとの接続に関連するコードは、一般的にはリポジトリ層に記述することが推奨されます。リポジトリ層はデータベースへのアクセスや永続化を担当するため、データベース接続の確立やクエリの実行などの詳細な実装は、この層に含まれます。

というわけで、repositoryディレクトリ配下にdatabaseディレクトリを作成し、ここに接続に関するコードをおきます。

repository/database/db_connection.go
package database

import (
	"fmt"
	"log"
	"os"

	"github.com/joho/godotenv"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func NewDB() *gorm.DB {
	if err := godotenv.Load(); err != nil {
		log.Fatalln(err)
	}

	url := fmt.Sprintf(
		"postgres://%s:%s@%s:%s/%s",
		os.Getenv("POSTGRES_USER"),
		os.Getenv("POSTGRES_PASSWORD"),
		os.Getenv("POSTGRES_HOST"),
		os.Getenv("POSTGRES_PORT"),
		os.Getenv("POSTGRES_DB"),
	)
	fmt.Println(url)
	db, err := gorm.Open(postgres.Open(url), &gorm.Config{})
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("データベースとの接続に成功しました")
	return db
}

func CloseDB(db *gorm.DB) {
	sqlDB, _ := db.DB()
	if err := sqlDB.Close(); err != nil {
		log.Fatalln(err)
	}
	fmt.Println("データベースとの接続を切断しました")
}

マイグレーション

次に、マイグレーションの処理を追加します。
gormの機能にAutoMigretionがあるので、そちらを利用します。

ChatGPTによると、

gormを使用してマイグレーションを行うためのコードは、通常、プロジェクトのエントリーポイントやコマンドラインツールに記述することが一般的です。

とのことなので、cmdディレクトリ内にmigrateディレクトリを作成し、migration.goを作成します。
コードは下記です。

cmd/migrate/migrate.go
package main

import (
	"fmt"
	"go-todo/internal/domain"
	"go-todo/internal/repository/database"
)

func main() {
	dbConn := database.NewDB()
	defer fmt.Println("マイグレーションが正常に実行されました")
	defer database.CloseDB(dbConn)
	dbConn.AutoMigrate(&domain.Todo{})
}

これで一通りの実装が完了したので、動作確認してみます。

実際に動かしてみる

まずはマイグレーションを行います。

go run ./cmd/migrate/migrate.go

データベースとの接続に成功しました
データベースとの接続を切断しました
マイグレーションが正常に実行されました

問題なくマイグレーションが実行できました。
念のため、DBにテーブルが作成されたか確認しておきます。
下記コマンドを実行し、Postgresのコンテナに入ります。

docker-compose exec db bash

コンテナに入れたら、下記コマンドを実行します。

0392e60b9a5f:/# psql -h db -p 5432 -U todo-user -d todos
Password for user todo-user:

パスワードを聞かれるので、docker-compose.ymlに定義しているパスワードを入力するとログインできます。
ログインできたら、下記コマンドを実行してください。

todos=# \dt
         List of relations
 Schema | Name  | Type  |   Owner   
--------+-------+-------+-----------
 public | todos | table | todo-user
(1 row)

問題なくテーブルが作成されていました。
では、実際にアプリケーションを起動してみます。

go-todo % go run ./cmd/main.go

データベースとの接続に成功しました

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.10.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080

問題なく起動できたようです。
試しにいくつかリクエストを送ってみます。

POST

リクエスト

curl -X POST -H "Content-Type: application/json" -d '{"title":"テスト1", "statu":"処理中"}' http://localhost:8080/todos

レスポンス

{"id":0,"title":"テスト1","status":"処理中","created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"}

Postgres

todos=# select * from todos;

 id |  title  | status |          created_at           |          updated_at           
----+---------+--------+-------------------------------+-------------------------------
  1 |  テスト1  |  処理中 | 2023-06-23 06:47:29.228533+00 | 2023-06-23 06:47:29.228533+00
(1 rows)

GET(All Todos)

リクエスト

curl -X GET -H "Content-Type: application/json" http://localhost:8080/todos

レスポンス

[{"id":1,"title":"テスト1","status":"処理中","created_at":"2023-06-23T15:47:29.228533+09:00","updated_at":"2023-06-23T15:47:29.228533+09:00"}]

PUT

リクエスト

curl -X PUT -H "Content-Type: application/json" -d '{"title":"テスト1", "status":"処理済み"}' http://localhost:8080/todos/1

レスポンス

{"id":1,"title":"テスト1","status":"処理済み","created_at":"2023-06-23T15:47:29.228533+09:00","updated_at":"2023-06-23T15:47:29.228533+09:00"}

Postgres

todos=# select * from todos;
 id |  title  |  status  |          created_at           |          updated_at           
----+---------+----------+-------------------------------+-------------------------------
  1 |  テスト1   |  処理済み | 2023-06-23 06:47:29.228533+00 | 2023-06-23 06:47:29.228533+00
(1 rows)

DELETE

リクエスト

curl -X DELETE -H "Content-Type: application/json" http://localhost:8080/todos/1

Postgres

todos=# select * from todos;
 id | title | status | created_at | updated_at 
----+-------+--------+------------+------------
(0 rows)

問題なさそうです!

まとめ

いかがだったでしょうか?
ChatGPTを利用してクリーンアーキテクチャの学習をしてみましたが、的外れな回答があったり、調べてみると間違ったコードがあったり、私の知識が足りなかったりで、実際はかなり時間がかかってしまいました。

今後も学習にChatGPTを活用するつもりですが、皆さんも学習に活用する際は、まずは書籍等で学習してから利用すること、間違った回答をしてきている前提で、自分でも調べてみることをおすすめします。(他の技術者の方も言われていると思いますが。)

また、今回Go言語を使用しましたが、まだ実務で使用したことがないため、「ここはこういう書き方ができるよ!」みたいなのがあれば、ご指摘いただけるとありがたいです。

最後までお付き合い頂き、ありがとうございました。

26
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
18