gin + gormでクリーンアーキテクチャなTodoアプリ Part1(Todo作成編)
はじめに
gin+gormで作成したTodoアプリを作成し、クリーンアーキテクチャに修正していく記事になります。
この記事は下記2つのPartで構成しています。
- Part1: Todoアプリの作成
- PArt2: クリーンアーキテクチャにリファクタリング
概要
この記事ではまず初めにgin + gormの基本的な使い方とCRUD処理をソースコードベースで説明します。
またhtmlの読み込み方法などを学び、このようなtodoアプリを作ることを目標としています。
gin
Ginは、Go(Golang)で開発された高速なHTTP Webフレームワークです。
JSONバリデーション、レンダリングなど、WebアプリケーションやAPIの開発に役立つ多数の機能を提供してくれます。
初期構築
公式 クイックスタートを見て進めます。
1. ginのインストール
$ go get -u github.com/gin-gonic/gin
2. ginの立ち上げ
下記をgo runすると、localhost:8080で立ち上がります。
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 0.0.0.0:8080 でサーバーを立てます。
}
3.動作確認
下記のようにcurlでmessageが取得できれば、立ち上げ成功です。
% curl localhost:8080/ping
{"message":"pong"}
gorm
Gormは、Go言語(Golang)で最も人気のあるORM(Object-Relational Mapping)ライブラリの一つです。データベースとの間でデータをやり取りする際の操作を簡単に行うためのツールです。GormはSQLクエリを直接書く代わりに、より直感的なAPIを提供してくれます。
要はデータベースの操作が容易になり、コードの可読性と保守性を向上させてくれるものです。
初期構築
gormを使用する際には初期構築としては、下記の3点を行う必要があります。
1. データベースの準備
こちらの構築方法については解いません。本記事では、mysqlをdocker-composeを使用して構築しております。もし、準備をされていない方は参考にしてDBを構築してもらえたらと思います。
設定ファイル
./docker-compose.yml
version: "3"
services:
db:
image: mysql
container_name: db
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: go_todo
TZ: "Asia/Tokyo"
volumes:
- ./db/my.cnf:/etc/mysql/conf.d/my.cnf
ports:
- 3306:3306
./db/my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
[client]
default-character-set=utf8mb4
動作確認
- dockerコンテナの立ち上げ確認
詳細
-
docker compose up
でビルドが正常に実施できる
% docker compose up
[+] Running 1/1
✔ Container db Created 0.1s
Attaching to db
db | 2024-04-20 20:29:14+09:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.3.0-1.el8 started.
- コンテナのステータスがUpとなり、立ち上がっている。
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
157b9653c1f6 mysql "docker-entrypoint.s…" 8 seconds ago Up 6 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp db
- コンテナに接続できる。
% docker exec -it 157b9653c1f6 bash
- mysqlの動作確認
詳細
※ dockerコンテナに接続していること
- 下記の情報でmysqlに接続を行う。
ユーザー名: root(mysqlの実行権限を持っているユーザーを指定)
パスワード: password
bash-4.4# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.3.0 MySQL Community Server - GPL
Copyright (c) 2000, 2024, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
- データベースにgo_todoが存在すること。useでgo_todoデータベースが使用できることを確認
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| go_todo |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.04 sec)
mysql> use go_todo
Database changed
2. モデルの構築
GORMは、一般的に使用されるフィールドを含むgorm.Modelという名前の事前定義された構造体を提供してくれます。
公式: 構築方法
GORMとDBドライバーのインストール
go get -u gorm.io/gorm
# sqlite
go get -u gorm.io/driver/sqlite
# mysql
go get -u "gorm.io/driver/mysql"
# postgres
go get -u "gorm.io/driver/postgres"
モデルの宣言
今回のTodoアプリでは下記のデータベーステーブルを使用します。
package models
import "gorm.io/gorm"
type Todo struct {
*gorm.Model
Content string `json:"content"`
}
gorm.Modelについて
GORMは、一般的に使用されるフィールドを含むgorm.Model という名前の事前定義された構造体を提供してくれています。
下記がgorm.Modelのフィールドです。
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
3. データベースとの接続
main.goを作成し、下記のようにコマンドを入力し、:8080でローカルホストを開くことができれば成功になります。
% DB=go_todo DB_USER=root DB_PASSWORD=password DB_HOST=localhost DB_PORT=3306 go run .
main.go
package main
import (
"fmt"
"log"
"os"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Todo struct {
*gorm.Model
Content string `json:"content"`
}
type DBConfig struct {
User string
Password string
Host string
Port int
Table string
}
func getDBConfig() DBConfig {
port, _ := strconv.Atoi(os.Getenv("DB_PORT"))
return DBConfig{
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Host: os.Getenv("DB_HOST"),
Port: port,
Table: os.Getenv("DB"),
}
}
func connectionDB() (*gorm.DB, error) {
config := getDBConfig();
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", config.User, config.Password, config.Host, config.Port, config.Table)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
return db, err
}
func main() {
r := gin.Default()
db, err := connectionDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Migrate the schema
err = db.AutoMigrate(&Todo{})
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
fmt.Println("Database connection and setup successful")
r.Run(":8080")
}
動作確認ログ
% DB=go_todo DB_USER=root DB_PASSWORD=password DB_HOST=localhost DB_PORT=3306 go run .
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
Database connection and setup successful
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
main.goの解説
- DBConfig: DBの接続情報を保存する構造体モデル
DBConfig
type DBConfig struct {
User string
Password string
Host string
Port int
Table string
}
- getDBConfig(): DBの接続情報を環境変数から取り出し返す
getDBConfig()
func getDBConfig() DBConfig {
port, _ := strconv.Atoi(os.Getenv("DB_PORT"))
return DBConfig{
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Host: os.Getenv("DB_HOST"),
Port: port,
Table: os.Getenv("DB"),
}
}
- connectionDB(): getDBConfig()で取得したDBの情報を用いて接続を行い、DBドライバーを返す。
connectionDB()
func connectionDB() (*gorm.DB, error) {
config := getDBConfig();
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", config.User, config.Password, config.Host, config.Port, config.Table)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
return db, err
}
- db.AutoMigrate(&Todo{}): モデル名の複数形名のテーブルを作成してくれる。main.goを実行すると、下記のようにtodosが作成されていることがわかる。
db.AutoMigrate()とテーブル追加の動作確認
- db.AutoMigrate()
// Migrate the schema
err = db.AutoMigrate(&Todo{})
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
- テーブル追加の動作確認
mysql> use go_todo
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+-------------------+
| Tables_in_go_todo |
+-------------------+
| todos |
+-------------------+
1 row in set (0.01 sec)
CRUDの処理
gin + gormを用いて、CRUDのREST APIを作成する。
- CRUD処理のAPIを追加
- CRUD処理の動作確認を行う。
APIを追加
下記のmain.goを使用する。
CRUD処理については、「listeners関数」に含まれている
ginを用いたREST APIの追加は公式: GET,POST,PUT,PATCH,DELETE,OPTIONS メソッドを使うを参照
main.go
package main
import (
"fmt"
"log"
"os"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"net/http"
"encoding/json"
)
type Todo struct {
*gorm.Model
Content string `json:"content"`
}
type DBConfig struct {
User string
Password string
Host string
Port int
Table string
}
func getDBConfig() DBConfig {
port, _ := strconv.Atoi(os.Getenv("DB_PORT"))
return DBConfig{
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Host: os.Getenv("DB_HOST"),
Port: port,
Table: os.Getenv("DB"),
}
}
func connectionDB() (*gorm.DB, error) {
config := getDBConfig();
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", config.User, config.Password, config.Host, config.Port, config.Table)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
return db, err
}
func errorDB(db *gorm.DB, c *gin.Context) bool {
if db.Error != nil {
log.Printf("Error todos: %v", db.Error)
c.AbortWithStatus(http.StatusInternalServerError)
return true // エラーがあったことを示す
}
return false // エラーがなかったことを示す
}
func listeners(r *gin.Engine, db *gorm.DB) {
r.GET("/todo/delete", func(c *gin.Context) {
id, _ := c.GetQuery("id")
result := db.Delete(&Todo{}, id)
if errorDB(result, c) { return }
})
r.POST("/todo/update", func(c *gin.Context) {
id, _ := strconv.Atoi(c.PostForm("id"))
content := c.PostForm("content")
var todo Todo
result := db.Where("id = ?", id).Take(&todo)
if errorDB(result, c) { return }
todo.Content = content
result = db.Save(&todo)
if errorDB(result, c) { return }
})
r.POST("/todo/create", func(c *gin.Context) {
content := c.PostForm("content")
fmt.Println(c.Request.PostForm, content)
result := db.Create(&Todo{Content: content})
if errorDB(result, c) { return }
})
r.GET("/todo/list", func(c *gin.Context) {
var todos []Todo
// Get all records
result := db.Find(&todos)
if errorDB(result, c) { return }
fmt.Println(json.NewEncoder(os.Stdout).Encode(todos))
c.JSON(http.StatusOK, todos)
})
r.GET("/todo/get", func(c *gin.Context) {
var todo Todo
id, _ := c.GetQuery("id")
result := db.First(&todo, id)
if errorDB(result, c) { return }
// JSON形式でレスポンスを返す
fmt.Println(json.NewEncoder(os.Stdout).Encode(todo))
c.JSON(http.StatusOK, todo)
})
}
func main() {
r := gin.Default()
db, err := connectionDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Migrate the schema
err = db.AutoMigrate(&Todo{})
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
listeners(r, db)
fmt.Println("Database connection and setup successful")
r.Run(":8080")
}
各CRUD処理について
それぞれのAPIの挙動を確認する。
シェルから、curlでAPIを呼び出し、mysqlのデータの状態を確認していく、
CREATE
レコードの作成方法については公式: レコードの作成で確認することができます。
また、クエリや文字列のパラメータ取得については、こちらが参考になる。
main.goの下記の部分が作成している箇所となっています。
r.POST("/todo/create", func(c *gin.Context) {
content := c.PostForm("content")
result := db.Create(&Todo{Content: content})
if errorDB(result, c) { return }
})
動作確認
/todo/createで正常にデータ作成が動作するか確認します。
確認方法
- データ追加のスクリプト
% curl localhost:8080/todo/create -X POST -H "multipart/form-data" -F 'content=zenn_todo'
- mysqlでデータが追加されていることを確認
mysql> select * from todos;
Empty set (0.01 sec)
mysql> select * from todos;
+----+-------------------------+-------------------------+------------+-----------+
| id | created_at | updated_at | deleted_at | content |
+----+-------------------------+-------------------------+------------+-----------+
| 1 | 2024-04-20 13:56:03.090 | 2024-04-20 13:56:03.090 | NULL | zenn_todo |
+----+-------------------------+-------------------------+------------+-----------+
1 row in set (0.00 sec)
READ
レコードの取得方法については公式: レコードの取得で確認することができます。
main.goの下記の部分が作成している箇所となっています。
r.GET("/todo/list", func(c *gin.Context) {
var todos []Todo
// Get all records
result := db.Find(&todos)
if errorDB(result, c) { return }
fmt.Println(json.NewEncoder(os.Stdout).Encode(todos))
c.JSON(http.StatusOK, todos)
})
r.GET("/todo/get", func(c *gin.Context) {
var todo Todo
id, _ := c.GetQuery("id")
result := db.First(&todo, id)
if errorDB(result, c) { return }
// JSON形式でレスポンスを返す
fmt.Println(json.NewEncoder(os.Stdout).Encode(todo))
c.JSON(http.StatusOK, todo)
})
動作確認
/todo/listと/todo/getで正常にデータの読み取りが動作するか確認します。
確認方法
- 一括取得
% curl localhost:8080/todo/list -X GET
[{"ID":1,"CreatedAt":"2024-04-20T13:56:03.09Z","UpdatedAt":"2024-04-20T17:36:30.119Z","DeletedAt":null,"content":"zenn_todo"},{"ID":2,"CreatedAt":"2024-04-20T14:07:28.673Z","UpdatedAt":"2024-04-20T14:07:28.673Z","DeletedAt":null,"content":"zenn_todo2"}]
- 指定したIDのtodoを取得
% curl "localhost:8080/todo/get?id=2" -X GET
{"ID":2,"CreatedAt":"2024-04-20T14:07:28.673Z","UpdatedAt":"2024-04-20T14:07:28.673Z","DeletedAt":null,"content":"zenn_todo2"}
UPDATE
レコードの更新方法については公式: レコードの更新で確認することができます。
main.goの下記の部分が作成している箇所となっています。
r.POST("/todo/update", func(c *gin.Context) {
id, _ := strconv.Atoi(c.PostForm("id"))
content := c.PostForm("content")
var todo Todo
result := db.Where("id = ?", id).Take(&todo)
if errorDB(result, c) { return }
todo.Content = content
result = db.Save(&todo)
if errorDB(result, c) { return }
})
動作確認
/todo/updateで正常にデータが更新できるか確認します。
確認方法
- データ更新と、更新したデータを取得するスクリプト
% curl localhost:8080/todo/update -X POST -H "multipart/form-data" -F "id=1" -F "content=zenn_todo_updated"
% curl "localhost:8080/todo/get?id=1"
{"ID":1,"CreatedAt":"2024-04-20T13:56:03.09Z","UpdatedAt":"2024-04-20T17:43:03.347Z","DeletedAt":null,"content":"zenn_todo_updated"}
DELETE
レコードの削除方法については公式: レコードの削除で確認することができます。
main.goの下記の部分が作成している箇所となっています。
r.GET("/todo/delete", func(c *gin.Context) {
id, _ := c.GetQuery("id")
result := db.Delete(&Todo{}, id)
if errorDB(result, c) { return }
})
動作確認
/todo/deleteで正常にデータを削除できるか確認します。
確認方法
- 削除した後に、listで確認する。ID=1が存在しないことがわかる。
% curl "http://localhost:8080/todo/destroy?id=1" -X GET
% curl localhost:8080/todo/list
[{"ID":2,"CreatedAt":"2024-04-20T14:07:28.673Z","UpdatedAt":"2024-04-20T14:07:28.673Z","DeletedAt":null,"content":"zenn_todo2"},{"ID":3,"CreatedAt":"2024-04-20T17:24:56.723Z","UpdatedAt":"2024-04-20T17:24:56.723Z","DeletedAt":null,"content":""}]
クライアント処理の追加
公式: HTML をレンダリングするあたりを参照しながら進めていく。
- main.goの変更
- 更新等を行った際にリダイレクト処理を追加
- ホームの/indexと編集ページの/editを追加
- htmlファイルの読み込み処理を追加
- htmlファイルの準備
main.goの変更
main.go全文
package main
import (
"fmt"
"log"
"os"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"net/http"
"encoding/json"
)
type Todo struct {
*gorm.Model
Content string `json:"content"`
}
type DBConfig struct {
User string
Password string
Host string
Port int
Table string
}
func getDBConfig() DBConfig {
port, _ := strconv.Atoi(os.Getenv("DB_PORT"))
return DBConfig{
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Host: os.Getenv("DB_HOST"),
Port: port,
Table: os.Getenv("DB"),
}
}
func connectionDB() (*gorm.DB, error) {
config := getDBConfig();
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True", config.User, config.Password, config.Host, config.Port, config.Table)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
return db, err
}
func errorDB(db *gorm.DB, c *gin.Context) bool {
if db.Error != nil {
log.Printf("Error todos: %v", db.Error)
c.AbortWithStatus(http.StatusInternalServerError)
return true // エラーがあったことを示す
}
return false // エラーがなかったことを示す
}
func listeners(r *gin.Engine, db *gorm.DB) {
r.GET("/todo/delete", func(c *gin.Context) {
id, _ := c.GetQuery("id")
result := db.Delete(&Todo{}, id)
if errorDB(result, c) { return }
c.Redirect(http.StatusMovedPermanently, "/index")
})
r.POST("/todo/update", func(c *gin.Context) {
id, _ := strconv.Atoi(c.PostForm("id"))
content := c.PostForm("content")
var todo Todo
result := db.Where("id = ?", id).Take(&todo)
if errorDB(result, c) { return }
todo.Content = content
result = db.Save(&todo)
if errorDB(result, c) { return }
c.Redirect(http.StatusMovedPermanently, "/index")
})
r.POST("/todo/create", func(c *gin.Context) {
content := c.PostForm("content")
fmt.Println(c.Request.PostForm, content)
result := db.Create(&Todo{Content: content})
if errorDB(result, c) { return }
c.Redirect(http.StatusMovedPermanently, "/index")
})
r.GET("/todo/list", func(c *gin.Context) {
var todos []Todo
// Get all records
result := db.Find(&todos)
if errorDB(result, c) { return }
fmt.Println(json.NewEncoder(os.Stdout).Encode(todos))
c.JSON(http.StatusOK, todos)
})
r.GET("/todo/get", func(c *gin.Context) {
var todo Todo
id, _ := c.GetQuery("id")
result := db.First(&todo, id)
if errorDB(result, c) { return }
// JSON形式でレスポンスを返す
fmt.Println(json.NewEncoder(os.Stdout).Encode(todo))
c.JSON(http.StatusOK, todo)
})
r.GET("/index", func(c *gin.Context) {
var todos []Todo
result := db.Find(&todos)
if errorDB(result, c) { return }
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "やることリスト",
"todos": todos,
})
})
//todo edit
r.GET("/edit", func(c *gin.Context) {
id, err := strconv.Atoi(c.Query("id"))
if err != nil {
log.Fatalln(err)
}
var todo Todo
db.Where("id = ?", id).Take(&todo)
c.HTML(http.StatusOK, "edit.html", gin.H{
"title": "Todoの編集",
"todo": todo,
})
})
}
func main() {
r := gin.Default()
db, err := connectionDB()
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// Migrate the schema
err = db.AutoMigrate(&Todo{})
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
r.LoadHTMLGlob("client/*")
listeners(r, db)
fmt.Println("Database connection and setup successful")
r.Run(":8080")
}
解説
- LoadHTMLGlobを行うことで、指定のパスのhtml(今回はclinet/*.html)を全て読み込んでいる
r.LoadHTMLGlob("client/*")
- ホームページと編集ページの追加
c.HTML(http.StatusOK, htmlファイル名, ...
により、LoadHTMLGlobで読み込んだhtmlファイルを指定することができる。
ソースコード
r.GET("/index", func(c *gin.Context) {
var todos []Todo
result := db.Find(&todos)
if errorDB(result, c) { return }
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "やることリスト",
"todos": todos,
})
})
//todo edit
r.GET("/edit", func(c *gin.Context) {
id, err := strconv.Atoi(c.Query("id"))
if err != nil {
log.Fatalln(err)
}
var todo Todo
db.Where("id = ?", id).Take(&todo)
c.HTML(http.StatusOK, "edit.html", gin.H{
"title": "Todoの編集",
"todo": todo,
})
})
- /create, /delete, /updateにはホームページへのリダイレクト処理を追加
c.Redirect(http.StatusMovedPermanently, "/index")
htmlファイル
client/index.html 全文
<html>
<body>
<h1 class="page-title">
{{ .title }}
</h1>
<form class="todo-form" action="/todo/create" method="POST">
<div class="form-header">todo作成</div>
<div class="form-body">
<textarea class="todo-textarea" name="content" id="content" placeholder="Todoを追加" rows="4"></textarea>
<button class="submit-button" type="submit">作成</button>
</div>
</form>
<hr class="separator" />
{{ range .todos }}
<div class="todo-item">
<p class="todo-content"><a class="edit-link" href="/edit?id={{.ID}}">{{ .ID }}:{{ .Content }}</a></p>
<a class="delete-link" href="/todo/delete?id={{ .ID }}">[削除]</a>
</div>
{{ end }}
</body>
</html>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f4f4f4;
color: #333;
padding: 20px;
}
.page-title {
color: #007BFF;
text-align: center;
}
.todo-form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
.form-header {
font-size: 18px;
color: #555;
margin-bottom: 10px;
}
.form-body {
margin-bottom: 20px;
}
.todo-textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
margin-bottom: 10px;
}
.submit-button {
background-color: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
float: right;
}
.submit-button:hover {
background-color: #218838;
}
.separator {
border-top: 1px solid #ccc;
margin-top: 20px;
}
.todo-item {
padding: 10px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin-top: 10px;
}
.edit-link, .delete-link {
color: #007BFF;
text-decoration: none;
}
.edit-link:hover, .delete-link:hover {
text-decoration: underline;
}
</style>
client/edit.html 全文
<html>
<body>
<h1 class="page-title">
{{ .title }}
</h1>
<form class="todo-form" action="/todo/update" method="POST">
<input type="hidden" name="id" value="{{.todo.ID}}" />
<div class="form-header">Todos更新</div>
<div class="form-body">
<textarea class="todo-textarea" name="content" placeholder="Todoを更新" rows="4">{{.todo.Content}}</textarea>
<button class="submit-button" type="submit">更新</button>
</div>
</form>
<hr class="separator" />
</body>
</html>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f4f4f4;
color: #333;
padding: 20px;
}
.page-title {
color: #007BFF;
text-align: center;
}
.todo-form {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
.form-header {
font-size: 18px;
color: #555;
margin-bottom: 10px;
}
.form-body {
margin-bottom: 20px;
}
.todo-textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
margin-bottom: 10px;
}
.submit-button {
background-color: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
display: block; /* Makes the button block level, which might be preferable in some designs */
width: 100%; /* Adjust if you want a smaller button */
}
.submit-button:hover {
background-color: #218838;
}
.separator {
border-top: 1px solid #ccc;
margin-top: 20px;
}
</style>
解説
- ginのHTMLの記述方はこちらが参考になりました。
- 今回は、{{ range .todos }}...{{ end }}とすることで、やることリストを全て表示しております。
- 基本的にGETメソッドはaタグ, POSTメソッドformタグでハンドリングしています。
動作確認
次回
今回は、gin + gormで基本的な書き方やCRUD処理を学びました。ginを用いてhtmlにレンダリングし、webアプリとして動かすことができました。
しかし、今回はmain.goで全て記載した為、アーキテクチャの観点では下記の問題点があります。
-
モデルの定義がORMに依存しすぎでいる
- データの形式を定義する部分が、データベース操作ライブラリ(ORM)に依存しているため、他のデータベース操作方法に移行しにくい状態。
-
コードの再利用性が低い
- コードが特定の状況に特化して書かれているため、他のプロジェクトで再利用しにくくなっています。
-
依存性注入がない
- 現状のmain.goのプログラムは、各部分が互いに強く依存しているため、変更やテストが困難になっています。
こういった観点を次回はクリーンアーキテクチャに置き換える形で直していきたいと思います。
Discussion