My Note

自己理解のためのブログ

ORMのgormを利用して Echo で サインアップとログイン機能を追加する

はじめに

サーバーサイドについて実際に手を動かして勉強するために簡単なアプリケーションを作成しました。 今回は、以前からEchoで開発しているAPIアプリを利用しViewとModelを追加してサインアップとログイン機能を実装。

DBの利用

今回はDockerでMySQLコンテナを利用しました。O/Rマッパーは gorm を利用しました。 gorm(MySQL)を利用するには、下記のようにimportします。

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

■ GORMのドキュメント gorm.io

■ GORMのGitHub github.com

  • model/db.go
package model

import (
    "log"

    "github.com/yhidetoshi/apiEchoGAE-local/conf"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var db *gorm.DB

const (
    dbEndpoint = conf.DB_USER + ":" + conf.DB_PASS + "@tcp(" + conf.DB_HOST + ":" + conf.DB_PORT + ")/" + conf.DB_NAME + "?charset=utf8mb4&parseTime=True&loc=Local"
)

func init() {
    var err error
    //dsn := "root:root@tcp(127.0.0.1:3306)/echo?charset=utf8mb4&parseTime=True&loc=Local"
    dsn := dbEndpoint
    db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Println(err)
    }

    // Migration //
    db.AutoMigrate(&User{})

    // ex)
    // カラム削除
    //db.Migrator().DropColumn(&User{}, "Test")
    // レコード追加
    //user := User{Name: "sample_user", Password: "hoge1"}
    //db.(&user)
}

dbユーザとパスは "app" などのユーザを作成して付与すべきですが、今はとりあえずrootで。

package conf

const (
    // MySQL
    DB_HOST = "localhost"
    DB_NAME = "echo"
    DB_PORT = "3306"
    DB_USER = "root"
    DB_PASS = "root"

    // Redis
    REDIS_HOST = "localhost"
    REDIS_PORT = "6379"
)

MySQLコンテナの用意

  • (ex) docker-compose.yml
version: '3'
services:
  mysql:
    build: ./docker/mysql/
    volumes:
      - ./docker/mysql/data:/docker-entrypoint-initdb.d 
      - ./docker/mysql/data:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    image: mysql
    ports:
      - "3306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=${PASSWORD}
  • (ex) ./docker/mysql/Dockerfile
FROM mysql:8.0.28-debian

ENV MYSQL_DATABASE=${DB_NAME}
ENV MYSQL_USER=${USER_NAME}
ENV MYSQL_PASSWORD=${USER_PASSWPRD}
ENV TZ='Asia/Tokyo'

#ポートを開ける
EXPOSE 3306

#MySQL設定ファイルをイメージ内にコピー
ADD ./my.cnf /etc/mysql/conf.d/my.cnf

#docker runに実行される
CMD ["mysqld"]
  • ./docker/mysql/my.cnf
[mysqld]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

Migrationについて

Migration | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

カラムの追加と削除

  • カラム "test" を追加する
type User struct {
    ID       int    `json:"id" gorm:"praimaly_key"`
    Name     string `json:"name"`
    Password string `json:"password"`
    Test     string `json:"test"`  // <---- Add
}
  • カラム "test" を削除する
    • User{}の構造体から削除して、以下のコードを追加したら削除できた。
db.Migrator().DropColumn(&User{}, "Test")
model/db.go:22 SLOW SQL >= 200ms
[254.879ms] [rows:0] ALTER TABLE `users` DROP COLUMN `Test`

レコードの追加と削除

以下のコードを追加して、ユーザとパスワードを追加。一括でレコードを追加する事も可能。

レコードの作成 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

user := User{Name: "sample_user", Password: "pass"}
db.Create(&user)

コード

ディレクトリ構成は以下。

❯ tree . -L 3
.
├── README.md
├── api
│   ├── base.go
│   ├── client.go
│   ├── crypto_coincheck.go
│   ├── crypto_coincheck_test.go
│   ├── crypto_gmo.go
│   ├── crypto_gmo_test.go
│   ├── healthcheck.go
│   ├── healthcheck_test.go
│   ├── investment_trust.go
│   └── metal.go
├── conf
│   └── config.go
├── docker
│   └── mysql
│       ├── Dockerfile
│       ├── data
│       └── my.cnf
├── docker-compose.yml
├── go.mod
├── go.sum
├── handler
│   ├── auth.go
│   └── top.go
├── main.go
├── model
│   ├── db.go
│   └── user.go
├── staticcheck.conf
└── view
    ├── login.html
    ├── signup.html
    └── top.html

参考) htmlページをレンダリングする事については、前回の記事で書きました。

yhidetoshi.hatenablog.com

/apiのコードについては、以前の記事にまとめています。

yhidetoshi.hatenablog.com

  • main.go
package main

import (
    "io"
    "net/http"
    "text/template"

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

    "github.com/yhidetoshi/apiEchoGAE-local/api"
    "github.com/yhidetoshi/apiEchoGAE-local/handler"
)

var e = createMux()

func createMux() *echo.Echo {
    e := echo.New()
    return e
}

type TemplateRender struct {
    templates *template.Template
}

func (t *TemplateRender) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
    return t.templates.ExecuteTemplate(w, name, data)
}

func main() {
    // Echoインスタンス作成 //
    http.Handle("/", e)
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.Gzip())

    cGmo := api.ClientCryptoGMO{}
    cGmo.NewClientCryptoGMO()
    cCc := api.ClientCryptoCoincheck{}
    cCc.NewClientCryptoCoincheck()

    // Template
    renderer := &TemplateRender{
        templates: template.Must(template.ParseGlob("view/*.html")),
    }
    e.Renderer = renderer

    // ルーティング //
    // ヘルスチェック
    apiG := e.Group("/api")
    apiG.GET("/healthcheck", api.Healthcheck) // /api/healthcheck (/apiを省略する形になる)

    // 貴金属の価格を取得
    apiG.GET("/metal", api.FetchMetal)

    // ISINコードを引数に基準価格を取得
    apiG.GET("/investment-trust/:isin", api.FetchInvestTrust)

    // 仮想通貨の価格を取得
    apiG.GET("/crypto/gmo", cGmo.FetchCryptoGMO)
    apiG.GET("/crypto/coincheck", cCc.FetchCryptoCoincheck)

    // HTMLページ
    e.GET("/", handler.ShowTopHTML)
    e.GET("/signup", handler.ShowSignUpHTML)
    e.POST("/signup", handler.SignUp)
    e.GET("/login", handler.ShowLoginHTML)
    e.POST("/login", handler.Login)

    e.Start(":8080")
}
  • ./handler/auth.go
    • サインアップとログイン機能
package handler

import (
    "html"
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/yhidetoshi/apiEchoGAE-local/model"
)

// FormData Loginフォームのデータ処理用
type FormData struct {
    Name     string
    Password string
    Message  string
}

// ResponseJSON レスポンスメッセージ
type ResponseJSON struct {
    Message string `json:"message"`
}

// ShowSignUpHTML サインアップのページ表示
func ShowSignUpHTML(c echo.Context) error {
    return c.Render(http.StatusOK, "signup", FormData{})
}

// ShowLoginHTML Loginページの表示
func ShowLoginHTML(c echo.Context) error {
    return c.Render(http.StatusOK, "login", FormData{})
}

// SignUp サインアップ処理
func SignUp(c echo.Context) error {
    signUpForm := FormData{
        Name:     c.FormValue("name"),
        Password: c.FormValue("password"),
    }

    name := html.EscapeString(signUpForm.Name)         // Form(Name)に入力されたデータを取得
    password := html.EscapeString(signUpForm.Password) // Form(Password)に入力されたデータを取得

    if name == "" || password == "" { // nullだけ弾く
        signUpForm.Message = "Invalid Name or Password"
        return c.Render(http.StatusOK, "signup", signUpForm)
    }

    user := new(model.User)
    user.Name = name
    user.Password = password

    err := c.Bind(user)
    if err != nil {
        return err
    }

    u := model.GetUser(&model.User{
        Name: user.Name,
    })
    if u.ID != 0 {
        return &echo.HTTPError{
            Code:    http.StatusConflict,
            Message: "Name already exists",
        }
    }

    model.CreateUser(user)
    user.Password = ""

    responseJSON := ResponseJSON{
        Message: "SignUp Success",
    }
    return c.JSON(http.StatusOK, responseJSON)
}

// Login ログイン処理
func Login(c echo.Context) error {
    loginForm := FormData{
        Name:     c.FormValue("name"),
        Password: c.FormValue("password"),
    }
    name := html.EscapeString(loginForm.Name)
    password := html.EscapeString(loginForm.Password)

    u := new(model.User)
    u.Name = name
    u.Password = password

    err := c.Bind(u)
    if err != nil {
        return err
    }

    user := model.GetUser(
        &model.User{Name: u.Name},
    )

    if u.Name != user.Name || u.Password != user.Password { // FormとDBのデータを比較
        return &echo.HTTPError{
            Code:    http.StatusUnauthorized,
            Message: "Invalid Name or Password",
        }
    }
    responseJSON := ResponseJSON{
        Message: "Login Success",
    }

    return c.JSON(http.StatusOK, responseJSON)
}

model

  • model/db.go は上記に記載済み

  • model/user.go

package model

type User struct {
    ID       int    `json:"id" gorm:"primaryKey"`
    Name     string `json:"name"`
    Password string `json:"password"`
}

func GetUser(u *User) User {
    var user User
    db.Where(u).First(&user)
    return user
}

func CreateUser(u *User) {
    db.Create(u)
}

view

  • view/signup.html
{{define "signup"}}
<!DOCTYPE html>
<html lang="jp">

<head>
    <meta charset="UTF-8">
    <title>サインアップページ</title>
</head>

<body>
    <form action="/signup" method="post">
        {{if ne .Message ""}}
        <p>{{.Message}}</p>
        {{end}}

        <p>Name</p>
        <p><input type="text" name="name" value="{{.Name}}"></p>

        <p>Password</p>
        <p><input type="password" name="password" value="{{.Password}}"></p>

        <p><input type="submit" value="Sign Up"></p>
    </form>
</body>

</html>
{{end}}
  • view/login.html
{{define "login"}}
<!DOCTYPE html>
<html lang="jp">

<head>
    <meta charset="UTF-8">
    <title>ログインページ</title>
</head>

<body>
    <form action="/login" method="post">
        {{if ne .Message ""}}
        <p>{{.Message}}</p>
        {{end}}

        <p>Name</p>
        <p><input type="text" name="name"></p>

        <p>Password</p>
        <p><input type="password" name="password"></p>

        <p><input type="submit" value="Login"></p>
    </form>
</body>

</html>
{{end}}

実行結果

  • http://127.0.0.1:8080/signup

  • http://127.0.0.1:8080/login

さいごに

今回は MySQLをコンテナで用意して、サインアップやログイン機能を追加しました。 次は、セッションを追加して、Redis管理などを実際に手を動かしながら学習していければと思います。