20200206のMySQLに関する記事は10件です。

CircleCI 2.1 設定サンプル(Node + MySQL)

環境

  • Circle CI 2.1
  • Node + MySQL(ですが、主にCircle CIの設定のため、←の環境には大きく依存していません)

設定例

version: 2.1

executors:
  default:
    docker:
      - image: circleci/node:10.18.0-buster
  extended:
    docker:
      - image: circleci/node:10.18.0-buster
      - image: circleci/mysql:5.7
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
          MYSQL_DATABASE: your_database_name_comes_here

commands:
  restore_node_dependencies:
    steps:
      - restore_cache:
          name: Restore node dependencies cache
          keys:
            - v1-node-dependencies-{{ checksum "yarn.lock" }}
            - v1-node-dependencies
  install_node_dependencies:
    steps:
      - run:
          name: Install node dependencies
          command: yarn install --frozen-lockfile
  save_node_dependencies:
    steps:
      - save_cache:
          name: Save node dependencies cache
          key: v1-node-dependencies-{{ checksum "yarn.lock" }}
          paths:
            - node_modules
  wait_for_db_start_up:
    steps:
      - run:
          name: Wait for db start up
          command: dockerize -wait tcp://127.0.0.1:3306 -timeout 1m
  run_test:
    steps:
      - run:
          name: Run test
          command: yarn run test

jobs:
  build:
    executor: default
    steps:
      - checkout
      - restore_node_dependencies
      - install_node_dependencies
      - save_node_dependencies
  test:
    executor: extended
    steps:
      - checkout
      - restore_node_dependencies
      - wait_for_db_start_up
      - run_test

workflows:
  build_and_test:
    jobs:
      - build
      - test:
          requires:
            - build

ポイント

  • 2.1の機能(executorscommands)を使って、設定をわかりやすくする。
  • executorを最適化する。
    • 例えば、DBのセットアップは数十秒かかるので(※実測で20秒程)、不要なところでは行わない。
    • 上記の例では、buildyarn install)にDBは不要なので、DBが無いexecutor(= default)を使っています。
    • executorの名前(defaultやextended)は任意です。
  • テスト実行前に、DBの起動待ちをする(dockerize -wait)。
  • step内でnameを付ける。
    • 無くてもいいですが、その場合はCircleCIのデフォルトが使われます。restore_cacheなどは全てRestoring Cacheとなり、中を見ないとどのステップか分からなくなるので、付けています。

参照先

yarn install--frozen-lockfile オプションを付ける

version: 2.1 では workflows の version 指定は不要。

その他

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQL基本コマンド1(DB・テーブル作成)

はじめに

会社でデータ分析業務を行っていて、SQLを書くようになったので備忘録として
コマンドをまとめていく
*コマンドは全て小文字でも可能
*必ず最後にセミコロンをつける

MYSQLを起動してログイン

*起動
sudo service mysqld start

*ログイン(rootユーザーとして)
mysql -u root

とりあえずDB・テーブル作成

*DB作成
create databese DB名;
*テーブル作成
create table table名(カラム名1 データ型1,カラム名2 データ型2.....);

何はともあれ最初はDBとテーブルを作成。
DB名・table名には好きな名前を入れてOK
複数DBがある場合は、use文を使用して使用するDBを決める

お次はカラムを作ろう

*usersテーブルの場合
 *カラム追加(単数追加)
 alter table users add カラム名 データ型;

*カラム追加(複数追加)
 alter table users add(カラム名1 データ型1,カラム名2 データ型2.......);

 *カラム削除
 alter table users drop カラム名;

 *カラム更新
 alter table change users 旧カラム名 新カラム名 データ型;

とりあえずここまで次はレコードの追加について書きたいと思う

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gorm.DB.FirstでIDしか取って来れなかった話

TL; DR

gorm.Open するときに &parseTime=trueパラメタをつけよう

問題

該当コード

var passedAuth AuthData
err := json.NewDecoder(r.Body).Decode(&passedAuth)
if err != nil {
    fmt.Println(err)
    return
}
var auth AuthData
db.First(&auth, &AuthData{UserID: passedAuth.UserID})
fmt.Printf("%+v\n", auth)

こうなって欲しかった

{Model:{ID:1 CreatedAt:2020-02-06 09:50:59 +0000 UTC UpdatedAt:2020-02-06 09:50:59 +0000 UTC DeletedAt:<nil>} UserID:hogehoge Password:hogehoge}

こうなった

{Model:{ID:1 CreatedAt:0001-01-01 00:00:00 +0000 UTC UpdatedAt:0001-01-01 00:00:00 +0000 UTC DeletedAt:<nil>} UserID: Password:}

IDしか取得できてない!

原因

よくログを見てみると...

sql: Scan error on column index 1, name "created_at": unsupported Scan, storing driver.Value type []uint8 into type *time.Time

教えてくれてた。無視しててごめん。

解決

見た感じ一般的なエラーっぽかったので、まるっとコピペしてググってみたらこの記事にたどり着いた。
Gormでunsupported Scan的なエラーが出た

どうやらgorm.Open するときに &parseTime=trueパラメタを付けないとらしい。

該当箇所を見てみる。

db, err := gorm.Open("mysql", "root:admin@tcp(mysql:3306)/data_base?charset=utf8mb4")

ついてない。これだ。修正。。

db, err := gorm.Open("mysql", "root:admin@tcp(mysql:3306)/data_base?charset=utf8mb4&parseTime=true")

直った。すごい。

参考文献

Gormでunsupported Scan的なエラーが出た

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ModelとTableを削除する

見通しの悪さゆえに良くモデルを作成したり削除してる。今回は削除の方法についてまとめた。

モデルの削除

rails destroy model モデル名

これでモデルのファイルやマイグレーションファイルが削除される。(カラムの追加などを行った後付けのマイグレーションファイルは削除されないため手動で削除する)

データベースに残ったテーブルを削除

rails dbconsole #mysqlを起動
show databases; #databaseの一覧が見れる
show table from データベース名; #テーブル一覧が見れる
drop table テーブル名;
quit;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ローカルでMysqlが急に動かなくなった

概要

いつも通りターミナルからmysqlコマンドでDBにログインしようとしたら以下のようなエラーがでた

$ mysql -u **** -D **** -h **.***.***.*** -p
dyld: Library not loaded: /usr/local/opt/openssl/lib/libssl.1.0.0.dylib
  Referenced from: /usr/local/bin/mysql
  Reason: image not found
Abort trap: 6

やったこと

ん~なんか調べてもよくわからん
結果いろんな記事見てったらbrewでインストールしなおすのが一番簡単で早いっぽい

1. アンインストール

$ brew uninstall mysql
Uninstalling /usr/local/Cellar/mysql/5.7.21... (323 files, 233.9MB)

2. インストール

$ brew install mysql
Updating Homebrew...


(以下略)

3. ログイン

$ mysql -u **** -D **** -h **.***.***.*** -p
Enter password:

動いた

事後に思ったこと

よくよく考えたらmysql起動しているかとか見るの忘れてた...
もしかしたらそれで解決したかも...

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go+Gin+Gorm】初心者だからちょっ早で超簡単webサービス作ってみる

なんとなくGo言語を書いてみたくなった。
Mac環境でやります。
劣化版Twitterアプリみたいなのを作ります。
ユーザー認証とかはないです。
今回は、

一覧・詳細表示、登録、削除、更新
バリデーション

を実装していきます。

これが今回作るwebサービスです。
つぶやき

Go言語とは?

これによると、

並列処理、ガベージコレクションを備え、軽快にコンパイルできる言語です。以下のような特徴を持っています:

- 一台のコンピュータ上であっという間に大型のGoプログラムをコンパイルすることができます。
- Goはソフトウェアの構造にモデルを与えます。分析をより簡単にこなすことができ、ファイルやライブラリのincludeといったCスタイルの書き出しにありがちな部分を大幅に省くことができます。
- Goは静的型付け言語です。型に階層の概念が無いのでユーザはその関係に気をとられることもなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。
- Goは完全なガベージコレクションタイプの言語です。また、基本的な並列処理とネットワークをサポートしています。
- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。

完全に理解した。

ginとは

go言語のフレームワークです。railsとかと違って密結合なオールインワンではないです。
なので、railsやってた方は拍子抜けするかも。
また、これくらいの規模ならFW使わなくても手軽にwebサービス作れちゃうのがgo言語のいい所だと理解しています。

gormとは

GO言語用のORMフレームワークです。
DB周りの処理を実装する時にこのフレームワークを使うと便利です。
ORMを使わないと、SQL文をコード内に書かないといけなくなるので、ぜひORM使いましょう。
ちなみにrailsだとActive RecordがORMにあたります。

ORMの恩恵がいまいちピンとこない方はこちらの記事をみるといいでしょう。
RailsのORM機能について

GoをローカルPCに入れてみる。

ここ見てやってみてください。
ターミナルでgoって二文字を打って、以下のようになってたらOK。
おめでとう!

$ go

Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildmode   build modes
    c           calling between Go and C
    cache       build and test caching
    environment environment variables
    filetype    file types
    go.mod      the go.mod file
    gopath      GOPATH environment variable
    gopath-get  legacy GOPATH go get
    goproxy     module proxy protocol
    importpath  import path syntax
    modules     modules, module versions, and more
    module-get  module-aware go get
    module-auth module authentication using go.sum
    module-private module configuration for non-public modules
    packages    package lists and patterns
    testflag    testing flags
    testfunc    testing functions

Use "go help <topic>" for more information about that topic.

MySQLをローカルにいれる。

railsとかと違って、MySQLは自分で用意しないといけない。はず。
以下のコードをターミナルで打ってMySQLに入れたらもうインストール済です。よかったですね。

$ mysql -uroot -p

入れなかった人は、インストールしましょう。
某はこちらの記事を見てインストールしました。
【超簡単】macへMySQLをインストール
ありがたやー。
インストールできたらターミナル画面はそのままで次へ

MySQLセットアップ

次はデータベースを作ったり、ユーザーを作ったりします。

データベース名:test

ユーザー名:test
パスワード:12345678

以下ターミナルに打ち込んでください。

// データベース作成
mysql> create database test;

// ユーザー作成
mysql> create user 'test'@'localhost' IDENTIFIED BY '12345678';

// データベースにアクセスする権限をユーザーに付与
mysql> grant all privileges on test.* to 'test'@'localhost';
mysql> flush privileges;
mysql> exit

テーブル作らなくていいの?と思った方。
後述のmain.go内のdbInit()でautoMigrateを使ってTweetsテーブルを自動で作ってます。
他にテーブルが必要な場合は、autoMigrateに都度書いていくという感じです。

ディレクトリ構成図

cd $GOPATH/srcして、以下のディレクトリやファイルを作ってください。
ファイルの中身は今は空で構いません。

mytweet/
┣ views/
┃  ┣ delete.html
┃  ┣ detail.html
┃  ┣ index.html
┗ main.go

外部ライブラリの導入

go getというコマンドを使います。
外部ライブラリを導入するには、go getコマンドを利用すると便利です。

go get パッケージ名
go getコマンドを発行すると、以下の処理が自動的に行われます。

・指定したパッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・依存パッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・ソースコードのビルド(go installコマンド相当)

参照:はじめてのGO言語
以下ターミナルで実行

// ginフレームワーク
$ go get github.com/gin-gonic/gin

// mysql用ドライバー
$ go get github.com/go-sql-driver/mysql

// gorm
$ go get github.com/jinzhu/gorm

go getした際にソースはGOPATH/src配下に、インストールされます。
ちなみに、実行ファイル(コンパイルされたやつ)はGOPATH/binにインストールされます。

完成版ソースコード一気見せ

完成したソースコードを載せます。いきなり雑になってごめんなさい。
さっき作成したファイルにコピペしてください。

main.go
package main

import (
    "log"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql" //直接的な記述が無いが、インポートしたいものに対しては"_"を頭につける決まり
    "github.com/jinzhu/gorm"
)

// Tweetモデル宣言
// モデルはDBのテーブル構造をGOの構造体で表したもの
type Tweet struct {
    gorm.Model
    Content string `form:"content" binding:"required"`
}

func gormConnect() *gorm.DB {
    DBMS := "mysql"
    USER := "test"
    PASS := "12345678"
    DBNAME := "test"
    // MySQLだと文字コードの問題で"?parseTime=true"を末尾につける必要がある
    CONNECT := USER + ":" + PASS + "@/" + DBNAME + "?parseTime=true"
    db, err := gorm.Open(DBMS, CONNECT)

    if err != nil {
        panic(err.Error())
    }
    return db
}

// DBの初期化
func dbInit() {
    db := gormConnect()

    // コネクション解放解放
    defer db.Close()
    db.AutoMigrate(&Tweet{}) //構造体に基づいてテーブルを作成
}

// データインサート処理
func dbInsert(content string) {
    db := gormConnect()

    defer db.Close()
    // Insert処理
    db.Create(&Tweet{Content: content})
}

//DB更新
func dbUpdate(id int, tweetText string) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    tweet.Content = tweetText
    db.Save(&tweet)
    db.Close()
}

// 全件取得
func dbGetAll() []Tweet {
    db := gormConnect()

    defer db.Close()
    var tweets []Tweet
    // FindでDB名を指定して取得した後、orderで登録順に並び替え
    db.Order("created_at desc").Find(&tweets)
    return tweets
}

//DB一つ取得
func dbGetOne(id int) Tweet {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Close()
    return tweet
}

//DB削除
func dbDelete(id int) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Delete(&tweet)
    db.Close()
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*.html")

    dbInit()

    //一覧
    router.GET("/", func(c *gin.Context) {
        tweets := dbGetAll()
        c.HTML(200, "index.html", gin.H{"tweets": tweets})
    })

    //登録
    router.POST("/new", func(c *gin.Context) {
        var form Tweet
        // ここがバリデーション部分
        if err := c.Bind(&form); err != nil {
            tweets := dbGetAll()
            c.HTML(http.StatusBadRequest, "index.html", gin.H{"tweets": tweets, "err": err})
            c.Abort()
        } else {
            content := c.PostForm("content")
            dbInsert(content)
            c.Redirect(302, "/")
        }
    })

    //投稿詳細
    router.GET("/detail/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic(err)
        }
        tweet := dbGetOne(id)
        c.HTML(200, "detail.html", gin.H{"tweet": tweet})
    })

    //更新
    router.POST("/update/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := c.PostForm("tweet")
        dbUpdate(id, tweet)
        c.Redirect(302, "/")
    })

    //削除確認
    router.GET("/delete_check/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := dbGetOne(id)
        c.HTML(200, "delete.html", gin.H{"tweet": tweet})
    })

    //削除
    router.POST("/delete/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        dbDelete(id)
        c.Redirect(302, "/")

    })

    router.Run()
}

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>一覧ページ</title>
  </head>
  <body>
    <header>
      <h1>つぶやき</h1>
    </header>
    <div class="wrap">
      <div class="input">
        <p>{{.err}}</p>
        <form action="/new" method="post">
          <p>いま思っていること :<input type="text" name="content" size="30" placeholder="つぶやくこと"/></p>
          <p><input type="submit" value="つぶやく" /></p>
        </form>
      </div>
      <div class="indexGet">
        <ul>
          {{range.tweets}}
          <li>
            {{.Content}}
            <label><a href="/detail/{{.ID}}">編集</a></label>
            <label><a href="/delete_check/{{.ID}}">削除</a></label>
          </li>
          {{end}}
        </ul>
      </div>
    </div>
  </body>
</html>
detail.html
<body>
    <h2>詳細</h2>

    <form method="post" action="/update/{{.tweet.ID}}">
        <p>内容<input type="text" name="tweet" size="30" value="{{.tweet.Content}}" ></p>
        <p><input type="submit" value="Send"></p>
    </form>
</body>
delete.html
<body>
    <h1>削除確認</h1>
    <p>本当に削除しますか?</p>
    <ul>
        <li>内容: {{.tweet.Content}}</li>
        <li>作成時間: {{.tweet.CreatedAt}}</li>
    </ul>

    <form method="post" action="/delete/{{.tweet.ID}}">
        <p><input type="submit" value="削除"></p>
        <p><a href="/">戻る</a></p>
    </form>
</body>

起動

mytweetディレクトリに入って、以下を実行!

go run main.go

http://localhost:8080
を開いて、できてたらOK!

go runコマンドとは?

Go言語はコンパイル言語ですが、go runコマンドを用いると一時ファイルとしてコンパイルしたプログラムをその場で実行することができます。
実行ファイルの作り方などはここを参照しましょう。

最後に

バリデーション部分がまだイマイチだなーという感じです。
今度はログイン機能などを追加してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみる

なんとなくGo言語を書いてみたくなった。
Mac環境でやります。
劣化版Twitterアプリみたいなのを作ります。
ユーザー認証とかはないです。
今回は、

・ 一覧・詳細表示、登録、削除、更新
・ 空の投稿ができないようにするバリデーション

を実装していきます。

これが今回作るwebサービスです。
つぶやき

Go言語とは?

これによると、

並列処理、ガベージコレクションを備え、軽快にコンパイルできる言語です。以下のような特徴を持っています:

- 一台のコンピュータ上であっという間に大型のGoプログラムをコンパイルすることができます。
- Goはソフトウェアの構造にモデルを与えます。分析をより簡単にこなすことができ、ファイルやライブラリのincludeといったCスタイルの書き出しにありがちな部分を大幅に省くことができます。
- Goは静的型付け言語です。型に階層の概念が無いのでユーザはその関係に気をとられることもなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。
- Goは完全なガベージコレクションタイプの言語です。また、基本的な並列処理とネットワークをサポートしています。
- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。

完全に理解した。

ginとは

go言語のフレームワークです。railsとかと違って密結合なオールインワンではないです。
なので、railsやってた方は拍子抜けするかも。
また、これくらいの規模ならFW使わなくても手軽にwebサービス作れちゃうのがgo言語のいい所だと理解しています。

gormとは

GO言語用のORMフレームワークです。
DB周りの処理を実装する時にこのフレームワークを使うと便利です。
ORMを使わないと、SQL文をコード内に書かないといけなくなるので、ぜひORM使いましょう。
ちなみにrailsだとActive RecordがORMにあたります。

ORMの恩恵がいまいちピンとこない方はこちらの記事をみるといいでしょう。
RailsのORM機能について

GoをローカルPCに入れてみる。

ここ見てやってみてください。
ターミナルでgoって二文字を打って、以下のようになってたらOK。
おめでとう!

$ go

Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildmode   build modes
    c           calling between Go and C
    cache       build and test caching
    environment environment variables
    filetype    file types
    go.mod      the go.mod file
    gopath      GOPATH environment variable
    gopath-get  legacy GOPATH go get
    goproxy     module proxy protocol
    importpath  import path syntax
    modules     modules, module versions, and more
    module-get  module-aware go get
    module-auth module authentication using go.sum
    module-private module configuration for non-public modules
    packages    package lists and patterns
    testflag    testing flags
    testfunc    testing functions

Use "go help <topic>" for more information about that topic.

MySQLをローカルにいれる。

railsとかと違って、MySQLは自分で用意しないといけない。はず。
以下のコードをターミナルで打ってMySQLに入れたらもうインストール済です。よかったですね。

$ mysql -uroot -p

入れなかった人は、インストールしましょう。
某はこちらの記事を見てインストールしました。
【超簡単】macへMySQLをインストール
ありがたやー。
インストールできたらターミナル画面はそのままで次へ

MySQLセットアップ

次はデータベースを作ったり、ユーザーを作ったりします。

データベース名:test

ユーザー名:test
パスワード:12345678

以下ターミナルに打ち込んでください。

// データベース作成
mysql> create database test;

// ユーザー作成
mysql> create user 'test'@'localhost' IDENTIFIED BY '12345678';

// データベースにアクセスする権限をユーザーに付与
mysql> grant all privileges on test.* to 'test'@'localhost';
mysql> flush privileges;
mysql> exit

テーブル作らなくていいの?と思った方。
後述のmain.go内のdbInit()でautoMigrateを使ってTweetsテーブルを自動で作ってます。
他にテーブルが必要な場合は、autoMigrateに都度書いていくという感じです。

ディレクトリ構成図

cd $GOPATH/srcして、以下のディレクトリやファイルを作ってください。
ファイルの中身は今は空で構いません。

mytweet/
┣ views/
┃  ┣ delete.html
┃  ┣ detail.html
┃  ┣ index.html
┗ main.go

外部ライブラリの導入

go getというコマンドを使います。
外部ライブラリを導入するには、go getコマンドを利用すると便利です。

go get パッケージ名
go getコマンドを発行すると、以下の処理が自動的に行われます。

・指定したパッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・依存パッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・ソースコードのビルド(go installコマンド相当)

参照:はじめてのGO言語
以下ターミナルで実行

// ginフレームワーク
$ go get github.com/gin-gonic/gin

// mysql用ドライバー
$ go get github.com/go-sql-driver/mysql

// gorm
$ go get github.com/jinzhu/gorm

go getした際にソースはGOPATH/src配下に、インストールされます。
ちなみに、実行ファイル(コンパイルされたやつ)はGOPATH/binにインストールされます。

完成版ソースコード一気見せ

完成したソースコードを載せます。いきなり雑になってごめんなさい。
さっき作成したファイルにコピペしてください。

main.go
package main

import (
    "log"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql" //直接的な記述が無いが、インポートしたいものに対しては"_"を頭につける決まり
    "github.com/jinzhu/gorm"
)

// Tweetモデル宣言
// モデルはDBのテーブル構造をGOの構造体で表したもの
type Tweet struct {
    gorm.Model
    Content string `form:"content" binding:"required"`
}

func gormConnect() *gorm.DB {
    DBMS := "mysql"
    USER := "test"
    PASS := "12345678"
    DBNAME := "test"
    // MySQLだと文字コードの問題で"?parseTime=true"を末尾につける必要がある
    CONNECT := USER + ":" + PASS + "@/" + DBNAME + "?parseTime=true"
    db, err := gorm.Open(DBMS, CONNECT)

    if err != nil {
        panic(err.Error())
    }
    return db
}

// DBの初期化
func dbInit() {
    db := gormConnect()

    // コネクション解放解放
    defer db.Close()
    db.AutoMigrate(&Tweet{}) //構造体に基づいてテーブルを作成
}

// データインサート処理
func dbInsert(content string) {
    db := gormConnect()

    defer db.Close()
    // Insert処理
    db.Create(&Tweet{Content: content})
}

//DB更新
func dbUpdate(id int, tweetText string) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    tweet.Content = tweetText
    db.Save(&tweet)
    db.Close()
}

// 全件取得
func dbGetAll() []Tweet {
    db := gormConnect()

    defer db.Close()
    var tweets []Tweet
    // FindでDB名を指定して取得した後、orderで登録順に並び替え
    db.Order("created_at desc").Find(&tweets)
    return tweets
}

//DB一つ取得
func dbGetOne(id int) Tweet {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Close()
    return tweet
}

//DB削除
func dbDelete(id int) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Delete(&tweet)
    db.Close()
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*.html")

    dbInit()

    //一覧
    router.GET("/", func(c *gin.Context) {
        tweets := dbGetAll()
        c.HTML(200, "index.html", gin.H{"tweets": tweets})
    })

    //登録
    router.POST("/new", func(c *gin.Context) {
        var form Tweet
        // ここがバリデーション部分
        if err := c.Bind(&form); err != nil {
            tweets := dbGetAll()
            c.HTML(http.StatusBadRequest, "index.html", gin.H{"tweets": tweets, "err": err})
            c.Abort()
        } else {
            content := c.PostForm("content")
            dbInsert(content)
            c.Redirect(302, "/")
        }
    })

    //投稿詳細
    router.GET("/detail/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic(err)
        }
        tweet := dbGetOne(id)
        c.HTML(200, "detail.html", gin.H{"tweet": tweet})
    })

    //更新
    router.POST("/update/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := c.PostForm("tweet")
        dbUpdate(id, tweet)
        c.Redirect(302, "/")
    })

    //削除確認
    router.GET("/delete_check/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := dbGetOne(id)
        c.HTML(200, "delete.html", gin.H{"tweet": tweet})
    })

    //削除
    router.POST("/delete/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        dbDelete(id)
        c.Redirect(302, "/")

    })

    router.Run()
}

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>一覧ページ</title>
  </head>
  <body>
    <header>
      <h1>つぶやき</h1>
    </header>
    <div class="wrap">
      <div class="input">
        <p>{{.err}}</p>
        <form action="/new" method="post">
          <p>いま思っていること :<input type="text" name="content" size="30" placeholder="つぶやくこと"/></p>
          <p><input type="submit" value="つぶやく" /></p>
        </form>
      </div>
      <div class="indexGet">
        <ul>
          {{range.tweets}}
          <li>
            {{.Content}}
            <label><a href="/detail/{{.ID}}">編集</a></label>
            <label><a href="/delete_check/{{.ID}}">削除</a></label>
          </li>
          {{end}}
        </ul>
      </div>
    </div>
  </body>
</html>
detail.html
<body>
    <h2>詳細</h2>

    <form method="post" action="/update/{{.tweet.ID}}">
        <p>内容<input type="text" name="tweet" size="30" value="{{.tweet.Content}}" ></p>
        <p><input type="submit" value="Send"></p>
    </form>
</body>
delete.html
<body>
    <h1>削除確認</h1>
    <p>本当に削除しますか?</p>
    <ul>
        <li>内容: {{.tweet.Content}}</li>
        <li>作成時間: {{.tweet.CreatedAt}}</li>
    </ul>

    <form method="post" action="/delete/{{.tweet.ID}}">
        <p><input type="submit" value="削除"></p>
        <p><a href="/">戻る</a></p>
    </form>
</body>

起動

mytweetディレクトリに入って、以下を実行!

go run main.go

http://localhost:8080
を開いて、できてたらOK!

go runコマンドとは?

Go言語はコンパイル言語ですが、go runコマンドを用いると一時ファイルとしてコンパイルしたプログラムをその場で実行することができます。
実行ファイルの作り方などはここを参照しましょう。

最後に

バリデーションメッセージがrailsのように親切じゃない。
自分でメッセージも用意しないといけないのかな。

今度はログイン機能などを追加してみたいと思います。

次回へ続く。。。かも。

参考

https://blog.kannart.co.jp/programming/2235/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【MySQL】カラムの追加

カラムの追加

ALTER TABLE 【テーブル名】 ADD COLUMN 【カラム名】 【型や制約】 COMMENT '【コメント】' AFTER 【カラム名】;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Aurora(MySQL互換)で、外部キーが絡んだINSERT/UPDATEによるデッドロックが検知されない問題

何が起きたのか

たまーにproduction環境でデッドロックが発生した

デッドロック発生時のログや各処理ごとで実行されるSQLのログから調査して
デッドロックが発生するクエリは特定できたがstagingやローカルでは再現せず

環境 DB
production MySQL互換Aurora
staging MySQL
development dockerのMySQL

各環境は上記の状態だったため
Aurora独自の何かがあるのでは?と思い検証をしてみることに

デッドロック発生後の対処

デッドロック発生時、mysqlコマンドでDBにつないで
原因となるクエリをKILLしてみたが解決せず
リードレプリカをフェイルオーバーさせることで対応した

検証開始

デッドロックが起きていたテーブル群

  • offers
  • offer_child_1
  • offer_child_2
  • documents
  • document_offer_child_2

※わかりやすいようにテーブル名は実際とは変えています

スキーマ

CREATE TABLE offers (
  id bigint(8) AUTO_INCREMENT PRIMARY KEY,
  title text
);
CREATE TABLE offer_child_1 (
  id bigint(8) AUTO_INCREMENT PRIMARY KEY,
  title text,
  offer_id bigint(8),
  FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE offer_child_2 (
  id bigint(8) AUTO_INCREMENT PRIMARY KEY,
  offer_id bigint(8),
  FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE documents (
  id bigint(8) AUTO_INCREMENT PRIMARY KEY,
  url text
);
CREATE TABLE document_offer_child_2 (
  id bigint(8) AUTO_INCREMENT PRIMARY KEY,
  document_id bigint(8),
  offer_child_2_id bigint(8),
  FOREIGN KEY (document_id) REFERENCES documents(id),
  FOREIGN KEY (offer_child_2_id) REFERENCES offer_child_2(id)
);

デッドロックが起きていたトランザクションのクエリ

事前に流すクエリ

INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
INSERT INTO offer_child_2 (offer_id) VALUES (1);
INSERT INTO documents(url) VALUES ('http://example.com');

トランザクション

No トランザクション1 トランザクション2
BEGIN; BEGIN;
1 UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
2 SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
3 INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
4 INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

なぜデッドロックが起きるのか

InnoDBの行レベルロックには共有ロック(IS)排他ロック(IX)の2種類がある
共有ロック(IS)同士では競合は発生しないがそれ以外の組み合わせでは競合(ロック待ち)が発生する

IS IX
IS 競合
IX 競合 競合

そして今回のケースでいうと
No.1UPDATE文は該当のレコードにIXを獲得
No.2SELECT FOR UPDATE文は該当のレコードIXを獲得
No.3INSERT文は外部キーの参照先(documentsoffer_child_2)のレコードにISを獲得
No.4INSERT文は外部キーの参照先(offers)のレコードにISを獲得
となるため

No.3No.2をロック待ち、No.4No.1をロック待ちをして
トランザクション1、2間でロックが交錯しデッドロックが発生する

各環境での挙動

事前にパラメータグループから

Aurora
innodb_lock_wait_timeout = 10
lock_wait_timeout = 15
MySQL(auroraに準拠)
autocommit = 0
innodb_lock_wait_timeout = 10
innodb_rollback_on_timeout = 0
innodb_table_locks = 1
lock_wait_timeout = 15

に設定しておく
innodb_rollback_on_timeoutinnodb_table_locks
Auroraにパラメータグループで設定できないためAuroraでのデフォルト値に準拠
SHOW VARIABLES;で確認可能

autocommitについてはAuroraのデフォルトが0で
デッドロック検知には0がいいらしいのでMySQL側も0に設定してAuroraに合わせる

ref: https://dev.mysql.com/doc/refman/5.6/ja/innodb-deadlock-detection.html

MySQL互換のAurora(5.7.12)

transaction1
mysql> BEGIN;
Query OK, 0 rows affected (0.05 sec)

mysql> UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
transaction2
mysql> BEGIN;
Query OK, 0 rows affected (0.03 sec)

mysql> SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
+----+----------+
| id | offer_id |
+----+----------+
|  1 |        1 |
+----+----------+
1 row in set (0.09 sec)

mysql> INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

transaction1transaction2もタイムアウトせず止まり続け
INSERTのプロセスが残ったままDBのCPUが100%に貼りつく

MySQL(5.7.16)(5.7.12がなかったので仕方なく。。。)

transaction1
mysql> BEGIN;
Query OK, 0 rows affected (0.02 sec)

mysql> UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
Query OK, 1 row affected (0.90 sec)
transaction2
mysql> BEGIN;
Query OK, 0 rows affected (0.03 sec)

mysql> SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
+----+----------+
| id | offer_id |
+----+----------+
|  1 |        1 |
+----+----------+
1 row in set (0.07 sec)

mysql> INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

transaction1INSERT INTO document_offer_child_2...は最初待たされるが
transaction2INSERT INTO offer_child_1...を実行すると
transaction2は瞬時にdeadlockが検知されてトランザクションがロールバックされて
transaction1の待ちがなくなりINSERTを完了する

Auroraの他のパターンのデッドロックでの検知の挙動を検証

※見やすくなるようSQLのみ書いていく 実行順はここまで同様transaction1/2交互

長いので折りたたんだ

全パターンを網羅しようとすると、
4クエリ(=2トランザクション×2クエリ)がそれぞれIS/IXのいずれかだとして
インテンションロックの組み合わせが16パターン

さらに
IXのDMLは SELECT FOR UPDATE / UPDATE
ISのDMLは INSERT / SELECT LOCK IN SHARE MODE
とすると、それぞれ2パターンとして
1つのインテンションロックの組み合わせの中でクエリの組み合わせは16パターン
全部で256(=16*16)パターンになる

また、IXにDELETE文も考慮するとクエリの組み合わせが36パターンになり
全部で576(16*36)パターンになってさすがにつらいので網羅せず
少しずつ条件を変えて検知できないデッドロックに作用してそうなクエリを探す

1. transaction1の先発クエリもSELECT FOR UPDATEにしてみる

結果: × デッドロックを検知せず止まり続ける
考察: 先発のUPDATEは関係なさそう

transaction1
BEGIN;
SELECT * from offers WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

2. transaction2の先発クエリもUPDATEにしてみる

結果: × デッドロックを検知せず止まり続ける
考察: 先発のSELECT FOR UPDATEは関係なさそう

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
transaction2
BEGIN;
UPDATE offer_child_2 SET offer_id = 1 WHERE id = 1;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

3. transaction1の後発クエリをSELECT LOCK IN SHARE MODEにしてみる

結果: ◯ transaction1はデッドロックを検知してロールバックし、transaction2は正常終了
考察: 後発が両トランザクションINSERTじゃないと検知しないデッドロックにならない?

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

4. transaction2の後発クエリをSELECT LOCK IN SHARE MODEにしてみる

結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 3とはtransaction2がロールバックされた、InnoDBの仕様(※1)か

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;

※1 https://dev.mysql.com/doc/refman/5.6/ja/innodb-deadlock-detection.html

小さいトランザクションを選択してロールバックしようと試みます

とあるので3のパターンではtransaction1が、今回はtransaction2が小さいと判断されたのだろう

ここまでで

  • transaction1/2いずれも後発がINSERTの時に検知されないデッドロックになることがわかった
  • では後発がいずれもSELECT/UPDATE/DELETEのときはどうだろうか

5. transaction1/2の後発クエリをSELECT LOCK IN SHARE MODEにしてみる

結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 後発がSELECTだとデッドロック検知してくれるらしい

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;

6. transaction1/2の後発クエリをUPDATEにしてみる

結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 後発がUPDATEでもデッドロック検知してくれるらしい

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
UPDATE offer_child_2 SET offer_id = 1 WHERE id = 1;
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;

7. transaction1/2の後発クエリをDELETEにしてみる

結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: 後発がDELETEでもデッドロック検知される INSERTだけがやはり特殊なのか

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
DELETE from offer_child_2 WHERE id = 1;
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
DELETE from offers WHERE id = 1;

8. transaction1/2の先発クエリをINSERTにしてみる

結果: ◯ transaction2はデッドロックを検知してロールバックし、transaction1は正常終了
考察: INSERTが後発であることに意味があるらしい

transaction1
BEGIN;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
transaction2
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;

結果

Auroraでは、transaction1/2ともに後発のクエリがINSERTの時に発生するデッドロックが
検知されず止まり続けることがわかった
また、検知されないパターンのデッドロックをMySQLで試すと検知される

気になることがあったので追加で検証

  • そもそもデッドロックではなく普通にINSERTがロック待ちした時ならちゃんとタイムアウトするのか
  • UPDATEの外部キー参照時の挙動はINSERTと異なるのか

AuroraとMySQLのINSERTUPDATEのロック待ちの挙動を検証

長いので折りたたんだ

9. IX獲得されたレコードを参照するINSERTをする

Aurora: 待ちが発生 タイムアウトもせず
MySQL: 待ちが発生 タイムアウトもせず

transaction1
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
transaction2
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

外部キーを持つレコードへのUPDATE時の待ちの挙動を検証

10. UPDATE対象レコードの外部キーは変更せずリレーションがないカラムを更新する

Aurora: 待ちなしで更新できた
MySQL: 待ちなしで更新できた

まず普通にデータ作る
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
transaction1
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
transaction2
-- id確認のSELECT
-- SELECT * FROM offer_child_1 WHERE offer_id = 1;
BEGIN;
UPDATE offer_child_1 SET title = CURRENT_TIMESTAMP WHERE id = 1;

11. UPDATE対象レコードの外部キーを変更すると

Aurora: 待ちが発生 タイムアウトもせず
MySQL: 待ちが発生 タイムアウトもせず

事前に
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
transaction1
-- id確認
-- SELECT * FROM offers;
BEGIN;
SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;
transaction2
-- id確認のSELECT
-- SELECT * FROM offer_child_1 WHERE offer_id = 1;
BEGIN;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;

12. デッドロック発生するクエリの後発クエリを、ロックされているレコードへ外部キーを変更するUPDATEにする

Aurora: デッドロック検知されず止まり続ける
MySQL: デッドロックを検知してロールバック

事前に流す
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
INSERT INTO offer_child_2 (offer_id) VALUES (2);

offer_child_1のid=1は10の検証で、
offersのid=2は11の検証で、
すでに作ったからここでは作らない

transaction1
-- id確認
-- SELECT * FROM document_offer_child_2 WHERE offer_child_2_id = 1;
BEGIN;
SELECT * from offers WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE document_offer_child_2 SET offer_child_2_id = 2 WHERE id = 1;
transaction2
-- id確認
-- SELECT * FROM offer_child_1 WHERE offer_id = 1;
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;

結果

MySQLでもAuroraでも外部キーの参照先がIXを取られていると
INSERTUPDATE(外部キー変更を含む時のみ)はロック待ちの時、タイムアウトしない
そして後発クエリがINSERTだけでなく、UPDATE(外部キー変更を含む時のみ)の場合も
Auroraはデッドロックを検知してくれない(MySQLはデッドロック検知してくれる)

さらに2点気になったので追加で検証

ref: https://dev.mysql.com/doc/refman/5.6/ja/innodb-foreign-key-constraints.html

  • InnoDBのINSERT時の挙動は外部キー参照先にISを取るという処理はAuroraも同じだろうか
  • だったらIS獲得されたレコードに対する外部キー参照のINSERTは待たずに実行されるよね?

あと
ref: https://dev.mysql.com/doc/refman/5.6/ja/innodb-parameters.html#sysvar_innodb_lock_wait_timeout

  • INSERT時にinnodb_lock_wait_timeoutが効かないってことは参照先をテーブルロックしようとしている可能性もある? ってことでこれらも一応検証してみる

長いので折りたたんだ

13. 親レコードのIS取って、それを参照するINSERTを書いてみる

Aurora: 待たずにINSERTができた
MySQL: 待たずにINSERTができた
考察: INSERT時の外部キー参照先に取るロックはISで間違いなさそう

transaction1
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;
transaction2
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

14. INSERTしたあと、そのレコードの外部キー参照先のテーブルの別レコードをIXでロックしてみる

Aurora: 待たずにSELECTできた
MySQL: 待たずにSELECTできた
考察: つまり参照先がテーブルロックの可能性もない

transaction1
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
transaction2
BEGIN;
SELECT * FROM offers WHERE id = 2 FOR UPDATE;

結果

予想通り、かつInnoDBのドキュメント通り外部キーの参照先へのロックはISだった

ここまでの結果をまとめて考察すると

  • INSERT時は外部キー参照先にはIS(行レベル共有ロック)を取るだけ
  • 外部キー変更のないUPDATEであれば外部キー参照先にIS取らないので参照先がロック取られてても待ちなく更新可能
  • MySQLでもAuroraでも、外部キーの参照先が先にロックが取られているときは外部キー参照を持つINSERT外部キーの変更があるUPDATEのロック待ちはタイムアウトしない
  • そしてAuroraでは後発クエリがそのようなINSERTUPDATEの時に発生するデッドロックはDBは検知してくれない
  • MySQLならそのようなデッドロックでも検知してロールバックしてくれる

ということがわかった

MySQL互換Auroraでの外部キーを持つINSERT外部キーの変更を行うUPDATEのロック待ちの挙動が危うそうなのでアプリケーションレイヤーでも実装時に以下のようなことに気をつけた方がいいかもしれない

  • 今回の事象の解決法としてはtransaction2INSERTの外部キー参照先(offers)を最初にロック取りに行けばデッドロックは起きなくなる
    • ロックを取る順番を合わせるっていう基本的なデッドロック対策ですね
      • 外部キー参照先のIS取ればシンプルに今回のデッドロックが解決
      • IXだと同時にINSERTUPDATEできなくなって同時アクセス時に待ちが発生する
      • どっちを取るかはケースバイケース
    • INSERTUPDATEだけのトランザクションであれば気にする必要はないが他のレコードもロックするような場合は注意が必要
    • 親レコードを更新して子レコード(外部キー持っている)更新するような処理があるとINSERT/UPDATE側が親をロックするようにする以外に回避策なさそう?
  • 中間テーブルのような外部キー参照が多いテーブルが多く、そこへのINSERT外部キー変更のUPDATEも多く、さらにその参照先もロックを取る処理があるアプリケーションは気をつけて実装しないと検知されないデッドロックで死ぬ可能性が高そう
  • スループットは落ちるがあえてMySQLを使うと少し安全かもしれない

ちなみにRailsでも共有ロック(IS)取れる

ただし、MySQL依存になってちょっと悲しい

Model.lock('LOCK IN SHARE MODE').find(...)

ref: https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html

タイムアウトしないとかデッドロック検知しない問題、何か解決法とか知ってる方いたらぜひコメントで教えて頂けると超助かります
※「このパラメータを設定すれば解決するよ」とかいうオチだったら恥ずかしすぎる。。。

おまけ編 PostgreSQL互換Auroraでも検証

長いので折りたたんだ

スキーマ

CREATE TABLE offers (
  id SERIAL PRIMARY KEY,
  title text
);
CREATE TABLE offer_child_1 (
  id SERIAL PRIMARY KEY,
  title text,
  offer_id bigint,
  FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE offer_child_2 (
  id SERIAL PRIMARY KEY,
  offer_id bigint,
  FOREIGN KEY (offer_id) REFERENCES offers(id)
);
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  url text
);
CREATE TABLE document_offer_child_2 (
  id SERIAL PRIMARY KEY,
  document_id bigint,
  offer_child_2_id bigint,
  FOREIGN KEY (document_id) REFERENCES documents(id),
  FOREIGN KEY (offer_child_2_id) REFERENCES offer_child_2(id)
);
事前に流すクエリ
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
INSERT INTO offer_child_2 (offer_id) VALUES (1);
INSERT INTO documents(url) VALUES ('http://example.com');

15. 最初のデッドロックのパターンを試す

Aurora: transaction2の後発クエリでロック待ちが発生しなかった
Postgres: transaction2の後発クエリでロック待ちが発生しなかった
考察: Postgresは行ロックの種類が多くUPDATEINSERTが競合しないようだ
ref: https://www.postgresql.jp/document/9.6/html/explicit-locking.html#locking-rows

transaction1
BEGIN;
UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);   
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

16. デッドロック再現のためUPDATESELECT FOR UPDATEに変える

Aurora: デッドロックを検知した
Postgres: デッドロックを検知した

transaction1
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);   
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

17. 後発クエリを外部キー変更のあるUPDATEにしてみる

データ用意
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');
INSERT INTO offers (title) VALUES (CURRENT_TIMESTAMP);
INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);
INSERT INTO offer_child_2 (offer_id) VALUES (2);

Aurora: デッドロックを検知した
Postgres: デッドロックを検知した

transaction1
BEGIN;
SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE document_offer_child_2 SET offer_child_2_id = 2 WHERE id = 1;
transaction2
BEGIN;
SELECT * FROM offer_child_2 WHERE id = 2 LIMIT 1 FOR UPDATE;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;

18. SELECT FOR UPDATEへのINSERTの待ちを検証

lock_timeoutを設定したかったがパラメータグループに項目がなく設定できなかったのでコンソールから設定
(これAWS RDSだと設定永続化できないのかな。。。)
postgresの`lock_timeoutはmsecなので3000(=3sec)で

Aurora: 3秒でロックがタイムアウトした
Postgres: 3秒でロックがタイムアウトした

transaction1
SET lock_timeout = 3000;
BEGIN;
SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;
transaction2
SET lock_timeout = 3000;
BEGIN;
INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');

19. SELECT FOR UPDATEへのUPDATEの待ちを検証

Aurora: 3秒でタイムアウトした
Postgres: 3秒でタイムアウトした

transaction1
SET lock_timeout = 3000;
BEGIN;
SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;
transaction2
SET lock_timeout = 3000;
BEGIN;
UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;(3)

結果

Postgresの方がロックのモードが多彩で暗黙的なロックで競合しづらいようなので安心かもしれない
また、Postgres互換のAuroraやPostgresでは
外部キーが絡んだINSERTUPDATEでのロック待ちはちゃんとタイムアウトするし
デッドロックが検知されない問題も起きなかった

結論

パターンとして見落としがあるかもしれないのでこれで完璧とは言えないが

  • MySQL互換のAuroraもMySQLも外部キーが絡んだINSERTUPDATEでタイムアウトしないロック待ちが発生する
  • MySQL互換のAuroraではさらにそのINSERT/UPDATEによるデッドロックが検知されない
  • Postgres互換のAuroraやPostgresはINSERT/UPDATEのデッドロック検知されたし
    • 外部キーが絡んだINSERTUPDATEによる待ちもちゃんとタイムアウトする
    • 行ロックの種類が多いのでMySQLよりもそもそも競合しづらい

デッドロック発生しやすさや発生時の挙動など考えて安全度が高そうなのは
Postgres互換Aurora = Postgres > MySQL > MySQL互換Aurora
といった感じかな

雑な追記

ふと検証してないことに気づいて
Aurora(MySQL互換)で外部キー参照先がロックされている状態でのDELETEを試したところ
正常にロックタイムアウトされた

2トランザクション2クエリでデッドロックを起こすパターンも
後続クエリを外部キーを先にロックされたDELETEにして試したが
両方のトランザクションともロックタイムアウトしたのでちょっと予想外だったけどまぁ問題ない挙動
やはりINSERT/UPDATEだけが異質

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者!current_userの使い方 〜ログイン中のユーザ名を表示〜

current_userでログイン中のユーザ名とメールアドレスの表示の仕方を紹介します。

書き始めるときはcurrent_userの前に=を忘れないでくださいね!

ユーザー名を表示 〜current_user〜

.header__left-box__list
  = current_user.user

スクリーンショット 2020-02-06 0.57.26.png

メールアドレスを表示させたい↓

.header__left-box__list
  = current_user.email   #nameからemailに変更

スクリーンショット 2020-02-06 0.58.08.png

なぜ、このように
= current_user.nameや= current_user.email
のようになるかというと、、、テーブルに注目。

   ↓

○usersテーブル

id name email
1 mirai mirai@mirai.com
2 : :
3 : :

nameカラムやemailカラムから引っ張ってきているのです!
=current_userの後ろには.を忘れず書く事!!

また、each doも同じです。

ではまた(^_^ )


おまけ

ユーザー名を表示 〜each do〜

.header__left-box__list
  - @group.users.each do |hhgg|
    = hhgg.name

スクリーンショット 2020-02-06 0.57.26.png

メールアドレスを表示させたい↓

.header__left-box__list
  - @group.users.each do |hhgg|
    = hhgg.email   #nameからemailに変更

スクリーンショット 2020-02-06 0.58.08.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む