- 投稿日:2020-02-06T20:00:55+09:00
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
の機能(executors
、commands
)を使って、設定をわかりやすくする。executor
を最適化する。
- 例えば、DBのセットアップは数十秒かかるので(※実測で20秒程)、不要なところでは行わない。
- 上記の例では、
build
(yarn install
)にDBは不要なので、DBが無いexecutor
(= default)を使っています。executor
の名前(defaultやextended)は任意です。- テスト実行前に、DBの起動待ちをする(
dockerize -wait
)。step
内でname
を付ける。
- 無くてもいいですが、その場合はCircleCIのデフォルトが使われます。
restore_cache
などは全てRestoring Cache
となり、中を見ないとどのステップか分からなくなるので、付けています。参照先
yarn install
に--frozen-lockfile
オプションを付ける
- https://circleci.com/docs/ja/2.0/yarn/
- https://classic.yarnpkg.com/ja/docs/cli/install/#toc-yarn-install-frozen-lockfile
version: 2.1
ではworkflows
の version 指定は不要。その他
- ソースコードが大きくなってきたら、ソースコードのキャッシュも検討する(※デフォルトでやっておいてもいいかもしれない)。
- 投稿日:2020-02-06T19:25:53+09:00
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 旧カラム名 新カラム名 データ型;とりあえずここまで次はレコードの追加について書きたいと思う
- 投稿日:2020-02-06T19:07:39+09:00
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")直った。すごい。
参考文献
- 投稿日:2020-02-06T16:46:00+09:00
ModelとTableを削除する
- 投稿日:2020-02-06T14:31:36+09:00
ローカルで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起動しているかとか見るの忘れてた...
もしかしたらそれで解決したかも...おわり
- 投稿日:2020-02-06T12:06:18+09:00
【Go+Gin+Gorm】初心者だからちょっ早で超簡単webサービス作ってみる
なんとなくGo言語を書いてみたくなった。
Mac環境でやります。
劣化版Twitterアプリみたいなのを作ります。
ユーザー認証とかはないです。
今回は、一覧・詳細表示、登録、削除、更新 バリデーションを実装していきます。
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/gormgo getした際にソースはGOPATH/src配下に、インストールされます。
ちなみに、実行ファイル(コンパイルされたやつ)はGOPATH/binにインストールされます。完成版ソースコード一気見せ
完成したソースコードを載せます。いきなり雑になってごめんなさい。
さっき作成したファイルにコピペしてください。main.gopackage 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.gohttp://localhost:8080
を開いて、できてたらOK!
go run
コマンドとは?Go言語はコンパイル言語ですが、go runコマンドを用いると一時ファイルとしてコンパイルしたプログラムをその場で実行することができます。
実行ファイルの作り方などはここを参照しましょう。最後に
バリデーション部分がまだイマイチだなーという感じです。
今度はログイン機能などを追加してみたいと思います。
- 投稿日:2020-02-06T12:06:18+09:00
【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみる
なんとなくGo言語を書いてみたくなった。
Mac環境でやります。
劣化版Twitterアプリみたいなのを作ります。
ユーザー認証とかはないです。
今回は、・ 一覧・詳細表示、登録、削除、更新 ・ 空の投稿ができないようにするバリデーションを実装していきます。
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/gormgo getした際にソースはGOPATH/src配下に、インストールされます。
ちなみに、実行ファイル(コンパイルされたやつ)はGOPATH/binにインストールされます。完成版ソースコード一気見せ
完成したソースコードを載せます。いきなり雑になってごめんなさい。
さっき作成したファイルにコピペしてください。main.gopackage 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.gohttp://localhost:8080
を開いて、できてたらOK!
go run
コマンドとは?Go言語はコンパイル言語ですが、go runコマンドを用いると一時ファイルとしてコンパイルしたプログラムをその場で実行することができます。
実行ファイルの作り方などはここを参照しましょう。最後に
バリデーションメッセージがrailsのように親切じゃない。
自分でメッセージも用意しないといけないのかな。今度はログイン機能などを追加してみたいと思います。
次回へ続く。。。かも。
参考
- 投稿日:2020-02-06T10:05:49+09:00
【MySQL】カラムの追加
- 投稿日:2020-02-06T04:13:57+09:00
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.1
のUPDATE
文は該当のレコードにIX
を獲得
No.2
のSELECT FOR UPDATE
文は該当のレコードIX
を獲得
No.3
のINSERT
文は外部キーの参照先(documents
とoffer_child_2
)のレコードにIS
を獲得
No.4
のINSERT
文は外部キーの参照先(offers
)のレコードにIS
を獲得
となるため
No.3
はNo.2
をロック待ち、No.4
はNo.1
をロック待ちをして
トランザクション1、2間でロックが交錯しデッドロックが発生する各環境での挙動
事前にパラメータグループから
Aurorainnodb_lock_wait_timeout = 10 lock_wait_timeout = 15MySQL(auroraに準拠)autocommit = 0 innodb_lock_wait_timeout = 10 innodb_rollback_on_timeout = 0 innodb_table_locks = 1 lock_wait_timeout = 15に設定しておく
innodb_rollback_on_timeout
とinnodb_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)
transaction1mysql> 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);transaction2mysql> 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');
transaction1
もtransaction2
もタイムアウトせず止まり続け
INSERTのプロセスが残ったままDBのCPUが100%に貼りつくMySQL(5.7.16)(5.7.12がなかったので仕方なく。。。)
transaction1mysql> 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)transaction2mysql> 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
transaction1
のINSERT INTO document_offer_child_2...
は最初待たされるが
transaction2
のINSERT 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
は関係なさそうtransaction1BEGIN; 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);transaction2BEGIN; 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
は関係なさそうtransaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);transaction2BEGIN; 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
じゃないと検知しないデッドロックにならない?transaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;transaction2BEGIN; 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)かtransaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);transaction2BEGIN; 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
だとデッドロック検知してくれるらしいtransaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;transaction2BEGIN; 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
でもデッドロック検知してくれるらしいtransaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; UPDATE offer_child_2 SET offer_id = 1 WHERE id = 1;transaction2BEGIN; 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
だけがやはり特殊なのかtransaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; DELETE from offer_child_2 WHERE id = 1;transaction2BEGIN; 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
が後発であることに意味があるらしいtransaction1BEGIN; INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1); UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1;transaction2BEGIN; 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の
INSERT
とUPDATE
のロック待ちの挙動を検証
長いので折りたたんだ
9.
IX
獲得されたレコードを参照するINSERT
をするAurora: 待ちが発生 タイムアウトもせず
MySQL: 待ちが発生 タイムアウトもせずtransaction1BEGIN; SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;transaction2BEGIN; 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');transaction1BEGIN; 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
を取られていると
INSERT
とUPDATE
(外部キー変更を含む時のみ)はロック待ちの時、タイムアウトしない
そして後発クエリが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
で間違いなさそうtransaction1BEGIN; SELECT * FROM offers WHERE id = 1 LIMIT 1 LOCK IN SHARE MODE;transaction2BEGIN; INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');14.
INSERT
したあと、そのレコードの外部キー参照先のテーブルの別レコードをIX
でロックしてみるAurora: 待たずに
SELECT
できた
MySQL: 待たずにSELECT
できた
考察: つまり参照先がテーブルロックの可能性もないtransaction1BEGIN; INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');transaction2BEGIN; SELECT * FROM offers WHERE id = 2 FOR UPDATE;結果
予想通り、かつInnoDBのドキュメント通り外部キーの参照先へのロックは
IS
だったここまでの結果をまとめて考察すると
INSERT
時は外部キー参照先にはIS
(行レベル共有ロック)を取るだけ外部キー変更のないUPDATE
であれば外部キー参照先にIS
取らないので参照先がロック取られてても待ちなく更新可能- MySQLでもAuroraでも、外部キーの参照先が先にロックが取られているときは
外部キー参照を持つINSERT
や外部キーの変更があるUPDATE
のロック待ちはタイムアウトしない- そしてAuroraでは後発クエリがそのような
INSERT
やUPDATE
の時に発生するデッドロックはDBは検知してくれない- MySQLならそのようなデッドロックでも検知してロールバックしてくれる
ということがわかった
MySQL互換Auroraでの
外部キーを持つINSERT
や外部キーの変更を行うUPDATE
のロック待ちの挙動が危うそうなのでアプリケーションレイヤーでも実装時に以下のようなことに気をつけた方がいいかもしれない
- 今回の事象の解決法としては
transaction2
がINSERT
の外部キー参照先(offers
)を最初にロック取りに行けばデッドロックは起きなくなる
- ロックを取る順番を合わせるっていう基本的なデッドロック対策ですね
- 外部キー参照先の
IS
取ればシンプルに今回のデッドロックが解決IX
だと同時にINSERT
やUPDATE
できなくなって同時アクセス時に待ちが発生する- どっちを取るかはケースバイケース
INSERT
やUPDATE
だけのトランザクションであれば気にする必要はないが他のレコードもロックするような場合は注意が必要- 親レコードを更新して子レコード(外部キー持っている)更新するような処理があると
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は行ロックの種類が多くUPDATE
とINSERT
が競合しないようだ
ref: https://www.postgresql.jp/document/9.6/html/explicit-locking.html#locking-rowstransaction1BEGIN; UPDATE offers SET title = CURRENT_TIMESTAMP WHERE id = 1; INSERT INTO document_offer_child_2 (document_id, offer_child_2_id) VALUES (1, 1);transaction2BEGIN; SELECT * FROM offer_child_2 WHERE id = 1 LIMIT 1 FOR UPDATE; INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');16. デッドロック再現のため
UPDATE
をSELECT FOR UPDATE
に変えるAurora: デッドロックを検知した
Postgres: デッドロックを検知したtransaction1BEGIN; 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);transaction2BEGIN; 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: デッドロックを検知したtransaction1BEGIN; SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE; UPDATE document_offer_child_2 SET offer_child_2_id = 2 WHERE id = 1;transaction2BEGIN; 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秒でロックがタイムアウトしたtransaction1SET lock_timeout = 3000; BEGIN; SELECT * FROM offers WHERE id = 1 LIMIT 1 FOR UPDATE;transaction2SET lock_timeout = 3000; BEGIN; INSERT INTO offer_child_1 (offer_id, title) VALUES (1, 'a');19.
SELECT FOR UPDATE
へのUPDATE
の待ちを検証Aurora: 3秒でタイムアウトした
Postgres: 3秒でタイムアウトしたtransaction1SET lock_timeout = 3000; BEGIN; SELECT * FROM offers WHERE id = 2 LIMIT 1 FOR UPDATE;transaction2SET lock_timeout = 3000; BEGIN; UPDATE offer_child_1 SET offer_id = 2 WHERE id = 1;(3)結果
Postgresの方がロックのモードが多彩で暗黙的なロックで競合しづらいようなので安心かもしれない
また、Postgres互換のAuroraやPostgresでは
外部キーが絡んだINSERT
やUPDATE
でのロック待ちはちゃんとタイムアウトするし
デッドロックが検知されない問題も起きなかった結論
パターンとして見落としがあるかもしれないのでこれで完璧とは言えないが
- MySQL互換のAuroraもMySQLも外部キーが絡んだ
INSERT
やUPDATE
でタイムアウトしないロック待ちが発生する- MySQL互換のAuroraではさらにその
INSERT
/UPDATE
によるデッドロックが検知されない- Postgres互換のAuroraやPostgresは
INSERT
/UPDATE
のデッドロック検知されたし
- 外部キーが絡んだ
INSERT
やUPDATE
による待ちもちゃんとタイムアウトする- 行ロックの種類が多いのでMySQLよりもそもそも競合しづらい
デッドロック発生しやすさや発生時の挙動など考えて安全度が高そうなのは
Postgres互換Aurora = Postgres > MySQL > MySQL互換Aurora
といった感じかな雑な追記
ふと検証してないことに気づいて
Aurora(MySQL互換)で外部キー参照先がロックされている状態でのDELETE
を試したところ
正常にロックタイムアウトされた2トランザクション2クエリでデッドロックを起こすパターンも
後続クエリを外部キーを先にロックされたDELETE
にして試したが
両方のトランザクションともロックタイムアウトしたのでちょっと予想外だったけどまぁ問題ない挙動
やはりINSERT/UPDATEだけが異質
- 投稿日:2020-02-06T01:35:42+09:00
初心者!current_userの使い方 〜ログイン中のユーザ名を表示〜
current_userでログイン中のユーザ名とメールアドレスの表示の仕方を紹介します。
書き始めるときは
current_user
の前に=
を忘れないでくださいね!ユーザー名を表示 〜current_user〜
.header__left-box__list = current_user.userメールアドレスを表示させたい↓
.header__left-box__list = current_user.email #nameからemailに変更なぜ、このように
= current_user.name
や= current_user
のようになるかというと、、、テーブルに注目。↓
○usersテーブル
id name
1 mirai mirai@mirai.com 2 : : 3 : :
name
カラムや
=current_user
の後ろには.
を忘れず書く事!!また、each doも同じです。
ではまた(^_^ )
おまけ
ユーザー名を表示 〜each do〜
.header__left-box__list - @group.users.each do |hhgg| = hhgg.nameメールアドレスを表示させたい↓
.header__left-box__list - @group.users.each do |hhgg| = hhgg.email #nameからemailに変更