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

MySQL SQLの結果に記載されている日本語が"?"で表示される

目的 SQLを実行した結果表示されるデータの日本語が?で表示されてしまう問題を解決する方法をメモ的に残す 方法 下記コマンドを実行してMySQLの設定ファイルのバックアップを取っておく。 $ cp /etc/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/mysql_org.cnf 下記コマンドを実行してMySQLの設定ファイルに追記できる状態にする。 $ sudo cat << EOF > /etc/mysql/conf.d/mysql.cnf 下記の内容を一行一行入力してEnterを押下する。 [mysql] [mysqld] character-set-server=utf8 [client] default-character-set=utf8 最後の行を入力し終わったら「ctrl」 + 「d」を押下して保存する。 MySQLを再起動する。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SQL] LeetCodeの無料問題集を活用して、SQLを使いこなし(6)

Q182 Duplicate Emails 質問 重複なメールアドレスを探します。 Code書き方のHint count関数とhavingを組み合わせて、重複のデータが探せます。 ※WHEREはCOUNT関数と一緒に使えない。 SELECT 列名 FROM テーブル名 GROUP BY 列名 HAVING COUNT(列名)>1; Code SELECT Email FROM Person GROUP BY Email HAVING COUNT(Email)>1; Result
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SQL] LeetCodeの無料問題集を活用して、SQLを使いこなし(5)

181 Employees Earning More Than Their Managers 質問 Employeeテーブルは、従業員のID、給料と彼らのマネージャーIDを含まれています。 Employeeで給料はマネージャーより高い従業員を探します。 Code SELECT a.NAME as Employee FROM Employee a LEFT JOIN Employee b ON a.ManagerID=b.ID WHERE a.Salary>b.Salary Result
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLの基本操作のまとめ

SQLを使ったことがないので、勉強メモを残します。 使用環境 windows10 MySQL8.0.24 基本操作 CREATE DATABASES:データベースの作成 SHOW DATABASES:データベースの一覧表示 CREATE TABLE:テーブル作成 SHOW CREATE TABLE:テーブルの表示 INSERT:テーブルにデータを挿入 SELECT:テーブルからデータを取得 UPDATE:データを上書き DELETE:データを削除 ALTER:テーブルにカラムを追加 CREATE DATABASES まず、テーブルを作る前に「PRACTICE」という名前のDATABASEを作成。 CREATE DATABASE [データベース名]; 実行結果 mysql> CREATE DATABASE PRACTICE; Query OK, 1 row affected (0.02 sec) SHOW DATABASES DATABASE一覧の表示。上記で作成した「practice」があることが確認できる。 mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | practice | | sakila | | sys | | world | +--------------------+ 7 rows in set (0.00 sec) CREATE TABLE 1.まず使用するデータベースを選択する。これを選択しないとテーブルの作成ができない。 USE [データベース名]; 実行結果 mysql> USE PRACTICE; Database changed 2.テーブルを作成する。今回はオプション無しで作成する。 CREATE TABLE [テーブル名] ( column1 [データ型] [その他オプション], column2 [データ型] [その他オプション], column3 [データ型] [その他オプション], ); 作成したのがこちらのTESTテーブル。 mysql> CREATE TABLE TEST( -> ID INTEGER, -> NAME TEXT, -> ENGLISH INTEGER, -> MATH INTEGER -> ); Query OK, 0 rows affected (0.07 sec) SHOW CREATE TABLE 前のCREATE TABLEで作成したテーブルを確認。 SHOW CREATE TABLE [テーブル名]; 実行結果 mysql> SHOW CREATE TABLE TEST; +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | TEST | CREATE TABLE `test` ( `ID` int DEFAULT NULL, `NAME` text, `ENGLISH` int DEFAULT NULL, `MATH` int DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.01 sec) INSERT 指定したテーブルにデータを挿入する。 INSERT INTO [テーブル名] ([column1], [column2], [column3]) VALUES ([値1], [値2], [値3]); 実行結果 mysql> INSERT INTO TEST (ID, NAME, ENGLISH, MATH) VALUES (1, 'RISA', 80, 90); Query OK, 1 row affected (0.01 sec) なお、以下のINSERT文(カラム指定なし)でもデータの挿入が可能。 mysql> INSERT INTO TEST VALUES (2, 'TARO', 75, 60); Query OK, 1 row affected (0.01 sec) SELECT SELECT * FROM [テーブル名]; 先ほどINSERTしたデータを確認。挿入したデータが登録されていることが確認できる。 mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 80 | 90 | | 2 | TARO | 75 | 60 | +------+------+---------+------+ 2 rows in set (0.00 sec) また、テーブルデータのカラムを指定して表示させる場合、以下の文で表示可能。 mysql> SELECT ID, NAME FROM TEST; +------+------+ | ID | NAME | +------+------+ | 1 | RISA | | 2 | TARO | +------+------+ 2 rows in set (0.00 sec) 条件付きで表示させる場合は、以下の文で表示可能。 mysql> SELECT * FROM TEST WHERE ID = 1; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 80 | 90 | +------+------+---------+------+ 1 row in set (0.00 sec) UPDATE UPDATE文はデータを更新するので、元のデータは消えてしまう。 WHERE句を使わずにデータを更新しようとすると、全てのデータを上書きしてしまう可能性あり。 今回は NAME = RISA の ENGLISH を 0 に上書きする。 UPDATE [テーブル名] SET [colum1] = [値1], [colum2] = [値2], ... WHERE [条件式]; mysql> UPDATE TEST SET ENGLISH = 0 WHERE NAME = "RISA"; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 更新ができているか確認のためSELECT文を実行。NAME = RISA の ENGLISH を 0 に上書きできた。 mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | +------+------+---------+------+ 2 rows in set (0.00 sec) DELETE DELETE文は条件を指定しないと、テーブル中の全てのレコードを削除してしまう。以下のようにWHERE句を組み合わせて、条件に当てはまるレコードだけを選択削除する。 元のデータは以下。 mysql> SELECT * FROM TEST; +------+-------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+-------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | | 3 | RENA | 65 | 100 | | 4 | ASUKA | 80 | 80 | +------+-------+---------+------+ 4 rows in set (0.00 sec) ここから ID = 4 のデータを削除する。 DELETE FROM [テーブル名] WHERE [条件式]; 実行結果と確認のためSELECT文実行。ID = 4 のデータが削除されていることが確認できる。 mysql> DELETE FROM TEST WHERE ID = 4; Query OK, 1 row affected (0.01 sec) mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | | 3 | RENA | 65 | 100 | +------+------+---------+------+ 3 rows in set (0.01 sec) レコードまるごとではなく、特定のカラムのデータのみ削除したい場合は、UPDATE文を使う。 「指定したカラムのデータをNULLに上書きする」といった処理を行う。NULLは「何もない」という意味を表す。 UPDATE [テーブル名] SET [column1] = NULL WHERE [条件式] ID = 3 の NAME を NULL にする。 mysql> UPDATE TEST SET NAME = NULL WHERE ID = 3; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | | 3 | NULL | 65 | 100 | +------+------+---------+------+ 3 rows in set (0.00 sec) ALTER テーブルにカラムを追加する場合、以下のALTER文で追加可能。 ALTER TABLE [テーブル名] ADD [column] [データ型] カラム名「SOCIETY」、データ型「INTEGER」の列を追加した結果が以下。 追加されたカラムにはNULLが入っている。 mysql> ALTER TABLE TEST ADD SOCIETY INTEGER; Query OK, 0 rows affected (0.06 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM TEST; +------+------+---------+------+---------+ | ID | NAME | ENGLISH | MATH | SOCIETY | +------+------+---------+------+---------+ | 1 | RISA | 0 | 90 | NULL | | 2 | TARO | 75 | 60 | NULL | | 3 | NULL | 65 | 100 | NULL | +------+------+---------+------+---------+ 3 rows in set (0.00 sec) 参考文献 以下の文献を参考にさせていただきました。ありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLの基本操作のまとめ①

SQLを使ったことがないので、勉強メモを残します。 使用環境 windows10 MySQL8.0.24 基本操作 CREATE DATABASES:データベースの作成 SHOW DATABASES:データベースの一覧表示 CREATE TABLE:テーブル作成 SHOW CREATE TABLE:テーブルの表示 INSERT:テーブルにデータを挿入 SELECT:テーブルからデータを取得 UPDATE:データを上書き DELETE:データを削除 ALTER:テーブルにカラムを追加 CREATE DATABASES まず、テーブルを作る前に「PRACTICE」という名前のDATABASEを作成。 CREATE DATABASE [データベース名]; 実行結果 mysql> CREATE DATABASE PRACTICE; Query OK, 1 row affected (0.02 sec) SHOW DATABASES DATABASE一覧の表示。上記で作成した「practice」があることが確認できる。 mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | practice | | sakila | | sys | | world | +--------------------+ 7 rows in set (0.00 sec) CREATE TABLE 1.まず使用するデータベースを選択する。これを選択しないとテーブルの作成ができない。 USE [データベース名]; 実行結果 mysql> USE PRACTICE; Database changed 2.テーブルを作成する。今回はオプション無しで作成する。 CREATE TABLE [テーブル名] ( column1 [データ型] [その他オプション], column2 [データ型] [その他オプション], column3 [データ型] [その他オプション], ); 作成したのがこちらのTESTテーブル。 mysql> CREATE TABLE TEST( -> ID INTEGER, -> NAME TEXT, -> ENGLISH INTEGER, -> MATH INTEGER -> ); Query OK, 0 rows affected (0.07 sec) SHOW CREATE TABLE 前のCREATE TABLEで作成したテーブルを確認。 SHOW CREATE TABLE [テーブル名]; 実行結果 mysql> SHOW CREATE TABLE TEST; +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | TEST | CREATE TABLE `test` ( `ID` int DEFAULT NULL, `NAME` text, `ENGLISH` int DEFAULT NULL, `MATH` int DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.01 sec) INSERT 指定したテーブルにデータを挿入する。 INSERT INTO [テーブル名] ([column1], [column2], [column3]) VALUES ([値1], [値2], [値3]); 実行結果 mysql> INSERT INTO TEST (ID, NAME, ENGLISH, MATH) VALUES (1, 'RISA', 80, 90); Query OK, 1 row affected (0.01 sec) なお、以下のINSERT文(カラム指定なし)でもデータの挿入が可能。 mysql> INSERT INTO TEST VALUES (2, 'TARO', 75, 60); Query OK, 1 row affected (0.01 sec) SELECT SELECT * FROM [テーブル名]; 先ほどINSERTしたデータを確認。挿入したデータが登録されていることが確認できる。 mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 80 | 90 | | 2 | TARO | 75 | 60 | +------+------+---------+------+ 2 rows in set (0.00 sec) また、テーブルデータのカラムを指定して表示させる場合、以下の文で表示可能。 mysql> SELECT ID, NAME FROM TEST; +------+------+ | ID | NAME | +------+------+ | 1 | RISA | | 2 | TARO | +------+------+ 2 rows in set (0.00 sec) 条件付きで表示させる場合は、以下の文で表示可能。 mysql> SELECT * FROM TEST WHERE ID = 1; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 80 | 90 | +------+------+---------+------+ 1 row in set (0.00 sec) UPDATE UPDATE文はデータを更新するので、元のデータは消えてしまう。 WHERE句を使わずにデータを更新しようとすると、全てのデータを上書きしてしまう可能性あり。 今回は NAME = RISA の ENGLISH を 0 に上書きする。 UPDATE [テーブル名] SET [colum1] = [値1], [colum2] = [値2], ... WHERE [条件式]; mysql> UPDATE TEST SET ENGLISH = 0 WHERE NAME = "RISA"; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 更新ができているか確認のためSELECT文を実行。NAME = RISA の ENGLISH を 0 に上書きできた。 mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | +------+------+---------+------+ 2 rows in set (0.00 sec) DELETE DELETE文は条件を指定しないと、テーブル中の全てのレコードを削除してしまう。以下のようにWHERE句を組み合わせて、条件に当てはまるレコードだけを選択削除する。 元のデータは以下。 mysql> SELECT * FROM TEST; +------+-------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+-------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | | 3 | RENA | 65 | 100 | | 4 | ASUKA | 80 | 80 | +------+-------+---------+------+ 4 rows in set (0.00 sec) ここから ID = 4 のデータを削除する。 DELETE FROM [テーブル名] WHERE [条件式]; 実行結果と確認のためSELECT文実行。ID = 4 のデータが削除されていることが確認できる。 mysql> DELETE FROM TEST WHERE ID = 4; Query OK, 1 row affected (0.01 sec) mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | | 3 | RENA | 65 | 100 | +------+------+---------+------+ 3 rows in set (0.01 sec) レコードまるごとではなく、特定のカラムのデータのみ削除したい場合は、UPDATE文を使う。 「指定したカラムのデータをNULLに上書きする」といった処理を行う。NULLは「何もない」という意味を表す。 UPDATE [テーブル名] SET [column1] = NULL WHERE [条件式] ID = 3 の NAME を NULL にする。 mysql> UPDATE TEST SET NAME = NULL WHERE ID = 3; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> SELECT * FROM TEST; +------+------+---------+------+ | ID | NAME | ENGLISH | MATH | +------+------+---------+------+ | 1 | RISA | 0 | 90 | | 2 | TARO | 75 | 60 | | 3 | NULL | 65 | 100 | +------+------+---------+------+ 3 rows in set (0.00 sec) ALTER テーブルにカラムを追加する場合、以下のALTER文で追加可能。 ALTER TABLE [テーブル名] ADD [column] [データ型] カラム名「SOCIETY」、データ型「INTEGER」の列を追加した結果が以下。 追加されたカラムにはNULLが入っている。 mysql> ALTER TABLE TEST ADD SOCIETY INTEGER; Query OK, 0 rows affected (0.06 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM TEST; +------+------+---------+------+---------+ | ID | NAME | ENGLISH | MATH | SOCIETY | +------+------+---------+------+---------+ | 1 | RISA | 0 | 90 | NULL | | 2 | TARO | 75 | 60 | NULL | | 3 | NULL | 65 | 100 | NULL | +------+------+---------+------+---------+ 3 rows in set (0.00 sec) 参考文献 以下の文献を参考にさせていただきました。ありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-gin + mysql + gormでAPIサーバーを立ててみた

簡易なウェブアプリが作りたくて、そのときのメモです。 go-ginでAPIサーバーを立ててフロント側と通信できるようにします。 OS macOS Catalina 使う技術スタック gin (ウェブサーバー) dlv (デバッグ) air (ホットリロード) gorm (ORMライブラリ) mysql golang-migrate (マイグレーション) docker あると便利なアプリ postman (rest api通信確認) goland (デバッグ、開発) sequel pro (データベース確認) ディレクトリ構造 最終的には以下のようになっています。 . ├── backend │   ├── Dockerfile.local │   ├── article │   │   └── article.go │   ├── go.mod │   ├── go.sum │   ├── handler │   │   ├── articleFunc.go │   │   └── userFunc.go │   ├── lib │   │   └── sql_handler.go │   ├── main.go │   ├── migration │   │   ├── main.go │   │   └── migrations │   │   ├── 1_articles.down.sql │   │   ├── 1_articles.up.sql │   │   ├── 2_users.down.sql │   │   └── 2_users.up.sql │   ├── tmp │   │   └── main │   └── user │   └── user.go ├── docker-compose.yml └── mysql ├── Dockerfile └── my.cnf 9 directories, 18 files 気をつけるところ github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダのところは各自書き換えてください。 ディレクトリ next-gin-mysqlという名前で立ち上げます。(nextをフロントエンドとして立ち上げたいのですが、記事の長さ的にそれは次回にします) $ mkdir next-gin-mysql バックエンド側 まずディレクトリを作ります。 next-gin-mysql $ mkdir backend go moduleの用意 go modulesの用意をします。 github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダで初期化するとやりやすいと思います。 next-gin-mysql/backend $ go mod init github.com/greenteabiscuit/next-gin-mysql/backend next-gin-mysql/backend $ go get -u github.com/gin-gonic/gin これで ginとその依存パッケージがダウンロードされます。 それぞれのファイルの用意 サーバーを立ち上げるmain.goです。データベース用のlib、記事とユーザーのモデル、そしてハンドラはあとで書きます。 main.go package main import ( "os" "time" "github.com/gin-gonic/gin" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/greenteabiscuit/next-gin-mysql/backend/handler" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/joho/godotenv" "github.com/gin-contrib/cors" ) func main() { if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { panic(err) } } article := article.New() user := user.New() lib.DBOpen() defer lib.DBClose() r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{ "http://localhost:3000", }, AllowMethods: []string{ "POST", "GET", "OPTIONS", }, AllowHeaders: []string{ "Access-Control-Allow-Credentials", "Access-Control-Allow-Headers", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", }, AllowCredentials: true, MaxAge: 24 * time.Hour, })) r.GET("/article", handler.ArticlesGet(article)) r.POST("/article", handler.ArticlePost(article)) r.POST("/user/login", handler.UserPost(user)) r.Run(os.Getenv("HTTP_HOST") + ":" + os.Getenv("HTTP_PORT")) // listen and serve on 0.0.0.0:8080 } 記事を扱うarticleのハンドラパッケージです。 handler/articleFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/gin-gonic/gin" ) func ArticlesGet(articles *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { result := articles.GetAll() c.JSON(http.StatusOK, result) } } type ArticlePostRequest struct { Title string `json:"title"` Description string `json:"description"` } func ArticlePost(post *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { requestBody := ArticlePostRequest{} c.Bind(&requestBody) item := article.Article{ Title: requestBody.Title, Description: requestBody.Description, } post.Add(item) c.Status(http.StatusNoContent) } } userのハンドラ関数は以下です。 handler/userFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/gin-gonic/gin" ) func UsersGet(users *user.Users) gin.HandlerFunc { return func(c *gin.Context) { result := users.GetAll() c.JSON(http.StatusOK, result) } } type UserPostRequest struct { Username string `json:"username"` Password string `json:"password"` } func UserPost(post *user.Users) gin.HandlerFunc { return func(c *gin.Context) { requestBody := UserPostRequest{} c.Bind(&requestBody) item := user.User{ Username: requestBody.Username, Password: requestBody.Password, } post.Add(item) c.Status(http.StatusNoContent) } } articleの構造体などを定義するファイルです。 article/article.go package article import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type Article struct { Title string `json:"title"` Description string `json:"description"` } type Articles struct { Items []Article } func New() *Articles { return &Articles{} } func (r *Articles) Add(a Article) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Articles) GetAll() []Article { db := lib.GetDBConn().DB var articles []Article if err := db.Find(&articles).Error; err != nil { return nil } return articles } userの構造体をまとめたファイルです。 user/user.go package user import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type User struct { Username string `json:"username"` Password string `json:"password"` } type Users struct { Items []User } func New() *Users { return &Users{} } func (r *Users) Add(a User) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Users) GetAll() []User { db := lib.GetDBConn().DB var users []User if err := db.Find(&users).Error; err != nil { return nil } return users } mysqlと接続を行う sql_handler.goも追加します。 lib/sql_handler.go package lib import ( "fmt" "os" "time" "gorm.io/gorm" "gorm.io/driver/mysql" ) // SQLHandler ... type SQLHandler struct { DB *gorm.DB Err error } var dbConn *SQLHandler // DBOpen は DB connectionを張る。 func DBOpen() { dbConn = NewSQLHandler() } // DBClose は DB connectionを張る。 func DBClose() { sqlDB, _ := dbConn.DB.DB() sqlDB.Close() } // NewSQLHandler ... func NewSQLHandler() *SQLHandler { user := os.Getenv("DB_USERNAME") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") fmt.Println(user, password, host, port) var db *gorm.DB var err error // Todo: USE_HEROKU = 1のときと場合分け if os.Getenv("USE_HEROKU") != "1" { dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbName + "?parseTime=true&loc=Asia%2FTokyo" db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } } /*else { var ( instanceConnectionName = os.Getenv("DB_CONNECTION_NAME") // e.g. 'project:region:instance' ) dbURI := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=true", user, password, instanceConnectionName, database) // dbPool is the pool of database connections. db, err = gorm.Open(mysql.Open(dbURI), &gorm.Config{}) if err != nil { panic(err) } }*/ sqlDB, _ := db.DB() //コネクションプールの最大接続数を設定。 sqlDB.SetMaxIdleConns(100) //接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。 sqlDB.SetMaxOpenConns(100) //接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。 sqlDB.SetConnMaxLifetime(100 * time.Second) sqlHandler := new(SQLHandler) db.Logger.LogMode(4) sqlHandler.DB = db return sqlHandler } // GetDBConn ... func GetDBConn() *SQLHandler { return dbConn } // BeginTransaction ... func BeginTransaction() *gorm.DB { dbConn.DB = dbConn.DB.Begin() return dbConn.DB } // Rollback ... func RollBack() { dbConn.DB.Rollback() } ローカル開発環境の整備 docker-composeで一発でサーバーをすべて立ち上げるようにします。このときについでに mysqlの準備とデバッグサーバーの準備もしておきます。 docker-compose.yml version: '3' services: go: build: context: ./backend dockerfile: Dockerfile.local volumes: - ./backend:/go/src/backend working_dir: /go/src/backend environment: TZ: Asia/Tokyo ports: - 8080:8080 - 2345:2345 security_opt: - apparmor:unconfined cap_add: - SYS_PTRACE mysql: build: ./mysql environment: TZ: Asia/Tokyo MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: backend ports: - 13306:3306 volumes: - mysql_volume:/var/lib/mysql volumes: mysql_volume: go用のDockerfileです。今後デプロイ用のDockerfileと分けるかもしれないので、一応Dockerfile.localにしておきます。 backend/Dockerfile.local FROM golang:1.15.2 COPY . /go/src/sample WORKDIR /go/src/sample RUN go get -u github.com/cosmtrek/air RUN go get -u github.com/go-delve/delve/cmd/dlv CMD ["air", "-c", ".air.toml"]** ホットリロードするために、airを使います。 airの設定ファイルである.air.tomlはairの本家レポジトリから引っ張ってくることができます。 backend/.air.toml # Config file for [Air](https://github.com/cosmtrek/air) in TOML format # Working directory # . or absolute path, please note that the directories following must be under root. root = "." tmp_dir = "tmp" [build] # Just plain old shell command. You could use `make` as well. cmd = "go build -o ./tmp/main ." # Binary file yields from `cmd`. bin = "tmp/main" # Customize binary. full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient" # Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "html"] # Ignore these filename extensions or directories. exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "storage"] # Watch these directories if you specified. include_dir = [] # Exclude files. exclude_file = [] # Exclude unchanged files. exclude_unchanged = true # This log file places in your tmp_dir. log = "air.log" # It's not necessary to trigger build each time file changes if it's too frequent. delay = 1000 # ms # Stop running old binary when build errors occur. stop_on_error = true # Send Interrupt signal before killing process (windows does not support this feature) send_interrupt = false # Delay after sending Interrupt signal kill_delay = 500 # ms [log] # Show log time time = false [color] # Customize each part's color. If no color found, use the raw app log. main = "magenta" watcher = "cyan" build = "yellow" runner = "green" [misc] # Delete tmp directory on exit clean_on_exit = true mysqlの設定ファイルです。 mysql/my.cnf # MySQLサーバーへの設定 [mysqld] # 文字コード/照合順序の設定 character_set_server=utf8mb4 collation_server=utf8mb4_bin # タイムゾーンの設定 default_time_zone=SYSTEM log_timestamps=SYSTEM # デフォルト認証プラグインの設定 default_authentication_plugin=mysql_native_password # mysqlオプションの設定 [mysql] # 文字コードの設定 default_character_set=utf8mb4 # mysqlクライアントツールの設定 [client] # 文字コードの設定 default_character_set=utf8mb4 mysqlのDockerfileです。 FROM mysql:8.0.21 # FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql && mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql COPY ./my.cnf /etc/mysql/conf.d/my.cnf 環境変数も設定しておきます。本番環境では見せないようにしましょう。 DB_HOSTはコンテナの名前で選ぶので、mysqlにしてあります。 backend/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend 立ち上げてみる サーバーを立ち上げてみましょう。以下のようにエラーが起きずに 2345が出てきたらとりあえずOKです。 docker-compose up --build mysql_1 | 2021-05-04T16:29:33.735276+09:00 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. mysql_1 | 2021-05-04T16:29:33.770064+09:00 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL. go_1 | watching user go_1 | building... go_1 | running... go_1 | API server listening at: [::]:2345 golandでの設定 golandで開発するのがやりやすいと思います。 今はデバッグのサーバしか立ち上がっていないので、ここで設定を行います。 delveのプロセスを走らせることで、APIサーバーも立ち上がるようになります。 メニューのdebugをクリックし、go remoteを選びます。 適当な名前をつけて、applyとdebugをクリックします。 これでコマンドラインでもサーバが立ち上がっています。ただまだマイグレーションができていないので、データベースなどからデータが取得できません。 go_1 | API server listening at: [::]:2345 go_1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. go_1 | go_1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. go_1 | - using env: export GIN_MODE=release go_1 | - using code: gin.SetMode(gin.ReleaseMode) go_1 | go_1 | [GIN-debug] GET /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlesGet.func1 (3 handlers) go_1 | [GIN-debug] POST /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlePost.func1 (3 handlers) go_1 | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default go_1 | [GIN-debug] Listening and serving HTTP on :8080 マイグレーション golang-migrateを使ってmysqlデータベースを立ち上げます。migrationというフォルダをbackend直下に作ります。 backend $ mkdir migration 以下のような構造になります。main.goでmigrationの処理を書きます。 migration $ tree . ├── main.go └── migrations ├── 1_articles.down.sql ├── 1_articles.up.sql ├── 2_users.down.sql └── 2_users.up.sql migrationの.env 先ほどと同じになりますが、なるべく見せないようにしましょう。今回はローカルなので大丈夫かと思いますが。 migration/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend migration の main.go migration/main.go package main import ( "database/sql" "flag" "fmt" "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate" "github.com/golang-migrate/migrate/database/mysql" _ "github.com/golang-migrate/migrate/source/file" "github.com/joho/godotenv" "github.com/pkg/errors" ) var migrationFilePath = "file://./migrations/" func main() { fmt.Println("start migration") flag.Parse() command := flag.Arg(0) migrationFileName := flag.Arg(1) if command == "" { showUsage() os.Exit(1) } if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { fmt.Println(errors.Wrap(err, "load error .env")) } } m := newMigrate() version, dirty, _ := m.Version() force := flag.Bool("f", false, "force execute fixed sql") if dirty && *force { fmt.Println("force=true: force execute current version sql") m.Force(int(version)) } switch command { case "new": newMigration(migrationFileName) case "up": up(m) case "down": down(m) case "drop": drop(m) case "version": showVersionInfo(m.Version()) default: fmt.Println("\nerror: invalid command '", command, "'") showUsage() os.Exit(0) } } func generateDsn() string { apiRevision := os.Getenv("API_REVISION") var dsn string if apiRevision == "release" { dsn = os.Getenv("DATABASE_URL") + "&multiStatements=true" // heroku対応 } else { user := os.Getenv("DB_USERNAME") pass := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, dbName) } return dsn } func newMigrate() *migrate.Migrate { dsn := generateDsn() db, openErr := sql.Open("mysql", dsn) if openErr != nil { fmt.Println(errors.Wrap(openErr, "error occurred. sql.Open()")) os.Exit(1) } driver, instanceErr := mysql.WithInstance(db, &mysql.Config{}) if instanceErr != nil { fmt.Println(errors.Wrap(instanceErr, "error occurred. mysql.WithInstance()")) os.Exit(1) } m, err := migrate.NewWithDatabaseInstance( migrationFilePath, "mysql", driver, ) if err != nil { fmt.Println(errors.Wrap(err, "error occurred. migrate.NewWithDatabaseInstance()")) os.Exit(1) } return m } func showUsage() { fmt.Println(` ------------------------------------- Usage: go run migration/main.go <command> Commands: new FILENAME Create new up & down migration files up Apply up migrations down Apply down migrations drop Drop everything version Check current migrate version -------------------------------------`) } func newMigration(name string) { if name == "" { fmt.Println("\nerror: migration file name must be supplied as an argument") os.Exit(1) } base := fmt.Sprintf("./migration/migrations/%s_%s", time.Now().Format("20060102030405"), name) ext := ".sql" createFile(base + ".up" + ext) createFile(base + ".down" + ext) } func createFile(fname string) { if _, err := os.Create(fname); err != nil { panic(err) } } func up(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Up() if err != nil { if err.Error() != "no change" { panic(err) } fmt.Println("\nno change") } else { fmt.Println("\nUpdated:") version, dirty, err := m.Version() showVersionInfo(version, dirty, err) } } func down(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Steps(-1) if err != nil { panic(err) } else { fmt.Println("\nUpdated:") showVersionInfo(m.Version()) } } func drop(m *migrate.Migrate) { err := m.Drop() if err != nil { panic(err) } else { fmt.Println("Dropped all migrations") return } } func showVersionInfo(version uint, dirty bool, err error) { fmt.Println("-------------------") fmt.Println("version : ", version) fmt.Println("dirty : ", dirty) fmt.Println("error : ", err) fmt.Println("-------------------") } マイグレーションのためのsqlファイル 1_articles.down.sql drop table if exists articles 1_articles.up.sql create table if not exists articles ( id integer auto_increment primary key, title varchar(40), description varchar(40) ) 2_users.down.sql drop table if exists users 2_users.up.sql create table if not exists users ( id integer auto_increment primary key, username varchar(40), password varchar(40) ) マイグレーションする dockerコンテナに入ってマイグレーションします。go run main.go upでマイグレーションが行われます。 $ docker exec -it container_name bash # cd migration # go run main.go up start migration Before: ------------------- version : 0 dirty : false error : no migration ------------------- Updated: ------------------- version : 2 dirty : false error : <nil> ------------------- これでdbでもテーブルができているのを確認したらOKです! Sequel Proなどのアプリで確認できます。 PostmanでのAPI確認 これは Postmanというデスクトップアプリで確認することができます。 Post JSON形式で送ります。 GET データベースにも hoge と hello worldが入っているのが確認できました。 まとめ air + gin + gorm + golang-migrate + mysql + delve + dockerで開発用APIサーバーを立ち上げることができました。 今後試してみたいこと herokuにデプロイしてみる フロントエンド(next + reactなど)とつなげてみる(そもそもレポジトリ名にnextを含んでいたので繋げようと思ったのですが、記事が長すぎてしまったので次回以降やりたいと思います。) 参考 go-ginでサクッとRESTAPIを構築する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-gin + mysql + gorm + Dockerで開発用APIサーバーを立ててみた

簡易なウェブアプリが作りたくて、そのときのメモです。 go-ginでAPIサーバーを立ててデータベースと通信できるようにします。 Github Repositoryを作りました。 OS macOS Catalina 使う技術スタック gin (ウェブサーバー) dlv (デバッグ) air (ホットリロード) gorm (ORMライブラリ) mysql golang-migrate (マイグレーション) docker あると便利なアプリ postman (rest api通信確認) goland (デバッグ、開発) sequel pro (データベース確認) ディレクトリ構造 最終的には以下のようになっています。 . ├── backend │   ├── Dockerfile.local │   ├── article │   │   └── article.go │   ├── go.mod │   ├── go.sum │   ├── handler │   │   ├── articleFunc.go │   │   └── userFunc.go │   ├── lib │   │   └── sql_handler.go │   ├── main.go │   ├── migration │   │   ├── main.go │   │   └── migrations │   │   ├── 1_articles.down.sql │   │   ├── 1_articles.up.sql │   │   ├── 2_users.down.sql │   │   └── 2_users.up.sql │   ├── tmp │   │   └── main │   └── user │   └── user.go ├── docker-compose.yml └── mysql ├── Dockerfile └── my.cnf 9 directories, 18 files 気をつけるところ github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダのところは各自書き換えてください。 ディレクトリ next-gin-mysqlという名前で立ち上げます。(nextをフロントエンドとして立ち上げたいのですが、記事の長さ的にそれは次回にします) $ mkdir next-gin-mysql バックエンド側 まずディレクトリを作ります。 next-gin-mysql $ mkdir backend go moduleの用意 go modulesの用意をします。 github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダで初期化するとやりやすいと思います。 next-gin-mysql/backend $ go mod init github.com/greenteabiscuit/next-gin-mysql/backend next-gin-mysql/backend $ go get -u github.com/gin-gonic/gin これで ginとその依存パッケージがダウンロードされます。 それぞれのファイルの用意 サーバーを立ち上げるmain.goです。データベース用のlib、記事とユーザーのモデル、そしてハンドラはあとで書きます。 main.go package main import ( "os" "time" "github.com/gin-gonic/gin" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/greenteabiscuit/next-gin-mysql/backend/handler" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/joho/godotenv" "github.com/gin-contrib/cors" ) func main() { if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { panic(err) } } article := article.New() user := user.New() lib.DBOpen() defer lib.DBClose() r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{ "http://localhost:3000", }, AllowMethods: []string{ "POST", "GET", "OPTIONS", }, AllowHeaders: []string{ "Access-Control-Allow-Credentials", "Access-Control-Allow-Headers", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", }, AllowCredentials: true, MaxAge: 24 * time.Hour, })) r.GET("/article", handler.ArticlesGet(article)) r.POST("/article", handler.ArticlePost(article)) r.POST("/user/login", handler.UserPost(user)) r.Run(os.Getenv("HTTP_HOST") + ":" + os.Getenv("HTTP_PORT")) // listen and serve on 0.0.0.0:8080 } 記事を扱うarticleのハンドラパッケージです。 handler/articleFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/gin-gonic/gin" ) func ArticlesGet(articles *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { result := articles.GetAll() c.JSON(http.StatusOK, result) } } type ArticlePostRequest struct { Title string `json:"title"` Description string `json:"description"` } func ArticlePost(post *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { requestBody := ArticlePostRequest{} c.Bind(&requestBody) item := article.Article{ Title: requestBody.Title, Description: requestBody.Description, } post.Add(item) c.Status(http.StatusNoContent) } } userのハンドラ関数は以下です。 handler/userFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/gin-gonic/gin" ) func UsersGet(users *user.Users) gin.HandlerFunc { return func(c *gin.Context) { result := users.GetAll() c.JSON(http.StatusOK, result) } } type UserPostRequest struct { Username string `json:"username"` Password string `json:"password"` } func UserPost(post *user.Users) gin.HandlerFunc { return func(c *gin.Context) { requestBody := UserPostRequest{} c.Bind(&requestBody) item := user.User{ Username: requestBody.Username, Password: requestBody.Password, } post.Add(item) c.Status(http.StatusNoContent) } } articleの構造体などを定義するファイルです。 article/article.go package article import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type Article struct { Title string `json:"title"` Description string `json:"description"` } type Articles struct { Items []Article } func New() *Articles { return &Articles{} } func (r *Articles) Add(a Article) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Articles) GetAll() []Article { db := lib.GetDBConn().DB var articles []Article if err := db.Find(&articles).Error; err != nil { return nil } return articles } userの構造体をまとめたファイルです。ちょっとここはテキトーなのでもう少し直したい、、、 user/user.go package user import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type User struct { Username string `json:"username"` Password string `json:"password"` } type Users struct { Items []User } func New() *Users { return &Users{} } func (r *Users) Add(a User) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Users) GetAll() []User { db := lib.GetDBConn().DB var users []User if err := db.Find(&users).Error; err != nil { return nil } return users } mysqlと接続を行う sql_handler.goも追加します。 lib/sql_handler.go package lib import ( "fmt" "os" "time" "gorm.io/gorm" "gorm.io/driver/mysql" ) // SQLHandler ... type SQLHandler struct { DB *gorm.DB Err error } var dbConn *SQLHandler // DBOpen は DB connectionを張る。 func DBOpen() { dbConn = NewSQLHandler() } // DBClose は DB connectionを張る。 func DBClose() { sqlDB, _ := dbConn.DB.DB() sqlDB.Close() } // NewSQLHandler ... func NewSQLHandler() *SQLHandler { user := os.Getenv("DB_USERNAME") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") fmt.Println(user, password, host, port) var db *gorm.DB var err error // Todo: USE_HEROKU = 1のときと場合分け if os.Getenv("USE_HEROKU") != "1" { dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbName + "?parseTime=true&loc=Asia%2FTokyo" db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } } /*else { var ( instanceConnectionName = os.Getenv("DB_CONNECTION_NAME") // e.g. 'project:region:instance' ) dbURI := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=true", user, password, instanceConnectionName, database) // dbPool is the pool of database connections. db, err = gorm.Open(mysql.Open(dbURI), &gorm.Config{}) if err != nil { panic(err) } }*/ sqlDB, _ := db.DB() //コネクションプールの最大接続数を設定。 sqlDB.SetMaxIdleConns(100) //接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。 sqlDB.SetMaxOpenConns(100) //接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。 sqlDB.SetConnMaxLifetime(100 * time.Second) sqlHandler := new(SQLHandler) db.Logger.LogMode(4) sqlHandler.DB = db return sqlHandler } // GetDBConn ... func GetDBConn() *SQLHandler { return dbConn } // BeginTransaction ... func BeginTransaction() *gorm.DB { dbConn.DB = dbConn.DB.Begin() return dbConn.DB } // Rollback ... func RollBack() { dbConn.DB.Rollback() } ローカル開発環境の整備 docker-composeで一発でサーバーをすべて立ち上げるようにします。このときについでに mysqlの準備とデバッグサーバーの準備もしておきます。 docker-compose.yml version: '3' services: go: build: context: ./backend dockerfile: Dockerfile.local volumes: - ./backend:/go/src/backend working_dir: /go/src/backend environment: TZ: Asia/Tokyo ports: - 8080:8080 - 2345:2345 security_opt: - apparmor:unconfined cap_add: - SYS_PTRACE mysql: build: ./mysql environment: TZ: Asia/Tokyo MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: backend ports: - 13306:3306 volumes: - mysql_volume:/var/lib/mysql volumes: mysql_volume: go用のDockerfileです。今後デプロイ用のDockerfileと分けるかもしれないので、一応Dockerfile.localにしておきます。 backend/Dockerfile.local FROM golang:1.15.2 COPY . /go/src/sample WORKDIR /go/src/sample RUN go get -u github.com/cosmtrek/air RUN go get -u github.com/go-delve/delve/cmd/dlv CMD ["air", "-c", ".air.toml"]** ホットリロードするために、airを使います。 airの設定ファイルである.air.tomlはairの本家レポジトリから引っ張ってくることができます。 backend/.air.toml # Config file for [Air](https://github.com/cosmtrek/air) in TOML format # Working directory # . or absolute path, please note that the directories following must be under root. root = "." tmp_dir = "tmp" [build] # Just plain old shell command. You could use `make` as well. cmd = "go build -o ./tmp/main ." # Binary file yields from `cmd`. bin = "tmp/main" # Customize binary. full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient" # Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "html"] # Ignore these filename extensions or directories. exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "storage"] # Watch these directories if you specified. include_dir = [] # Exclude files. exclude_file = [] # Exclude unchanged files. exclude_unchanged = true # This log file places in your tmp_dir. log = "air.log" # It's not necessary to trigger build each time file changes if it's too frequent. delay = 1000 # ms # Stop running old binary when build errors occur. stop_on_error = true # Send Interrupt signal before killing process (windows does not support this feature) send_interrupt = false # Delay after sending Interrupt signal kill_delay = 500 # ms [log] # Show log time time = false [color] # Customize each part's color. If no color found, use the raw app log. main = "magenta" watcher = "cyan" build = "yellow" runner = "green" [misc] # Delete tmp directory on exit clean_on_exit = true mysqlの設定ファイルです。 mysql/my.cnf # MySQLサーバーへの設定 [mysqld] # 文字コード/照合順序の設定 character_set_server=utf8mb4 collation_server=utf8mb4_bin # タイムゾーンの設定 default_time_zone=SYSTEM log_timestamps=SYSTEM # デフォルト認証プラグインの設定 default_authentication_plugin=mysql_native_password # mysqlオプションの設定 [mysql] # 文字コードの設定 default_character_set=utf8mb4 # mysqlクライアントツールの設定 [client] # 文字コードの設定 default_character_set=utf8mb4 mysqlのDockerfileです。 FROM mysql:8.0.21 # FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql && mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql COPY ./my.cnf /etc/mysql/conf.d/my.cnf 環境変数も設定しておきます。本番環境では見せないようにしましょう。 DB_HOSTはコンテナの名前で選ぶので、mysqlにしてあります。 backend/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend 立ち上げてみる サーバーを立ち上げてみましょう。以下のようにエラーが起きずに 2345が出てきたらとりあえずOKです。 docker-compose up --build mysql_1 | 2021-05-04T16:29:33.735276+09:00 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. mysql_1 | 2021-05-04T16:29:33.770064+09:00 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL. go_1 | watching user go_1 | building... go_1 | running... go_1 | API server listening at: [::]:2345 golandでの設定 golandで開発するのがやりやすいと思います。 今はデバッグのサーバしか立ち上がっていないので、ここで設定を行います。 delveのプロセスを走らせることで、APIサーバーも立ち上がるようになります。 メニューのdebugをクリックし、go remoteを選びます。 適当な名前をつけて、applyとdebugをクリックします。 これでコマンドラインでもサーバが立ち上がっています。ただまだマイグレーションができていないので、データベースなどからデータが取得できません。 go_1 | API server listening at: [::]:2345 go_1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. go_1 | go_1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. go_1 | - using env: export GIN_MODE=release go_1 | - using code: gin.SetMode(gin.ReleaseMode) go_1 | go_1 | [GIN-debug] GET /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlesGet.func1 (3 handlers) go_1 | [GIN-debug] POST /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlePost.func1 (3 handlers) go_1 | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default go_1 | [GIN-debug] Listening and serving HTTP on :8080 マイグレーション golang-migrateを使ってmysqlデータベースを立ち上げます。migrationというフォルダをbackend直下に作ります。 backend $ mkdir migration 以下のような構造になります。main.goでmigrationの処理を書きます。 migration $ tree . ├── main.go └── migrations ├── 1_articles.down.sql ├── 1_articles.up.sql ├── 2_users.down.sql └── 2_users.up.sql migrationの.env 先ほどと同じになりますが、なるべく見せないようにしましょう。今回はローカルなので大丈夫かと思いますが。 migration/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend migration の main.go migration/main.go package main import ( "database/sql" "flag" "fmt" "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate" "github.com/golang-migrate/migrate/database/mysql" _ "github.com/golang-migrate/migrate/source/file" "github.com/joho/godotenv" "github.com/pkg/errors" ) var migrationFilePath = "file://./migrations/" func main() { fmt.Println("start migration") flag.Parse() command := flag.Arg(0) migrationFileName := flag.Arg(1) if command == "" { showUsage() os.Exit(1) } if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { fmt.Println(errors.Wrap(err, "load error .env")) } } m := newMigrate() version, dirty, _ := m.Version() force := flag.Bool("f", false, "force execute fixed sql") if dirty && *force { fmt.Println("force=true: force execute current version sql") m.Force(int(version)) } switch command { case "new": newMigration(migrationFileName) case "up": up(m) case "down": down(m) case "drop": drop(m) case "version": showVersionInfo(m.Version()) default: fmt.Println("\nerror: invalid command '", command, "'") showUsage() os.Exit(0) } } func generateDsn() string { apiRevision := os.Getenv("API_REVISION") var dsn string if apiRevision == "release" { dsn = os.Getenv("DATABASE_URL") + "&multiStatements=true" // heroku対応 } else { user := os.Getenv("DB_USERNAME") pass := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, dbName) } return dsn } func newMigrate() *migrate.Migrate { dsn := generateDsn() db, openErr := sql.Open("mysql", dsn) if openErr != nil { fmt.Println(errors.Wrap(openErr, "error occurred. sql.Open()")) os.Exit(1) } driver, instanceErr := mysql.WithInstance(db, &mysql.Config{}) if instanceErr != nil { fmt.Println(errors.Wrap(instanceErr, "error occurred. mysql.WithInstance()")) os.Exit(1) } m, err := migrate.NewWithDatabaseInstance( migrationFilePath, "mysql", driver, ) if err != nil { fmt.Println(errors.Wrap(err, "error occurred. migrate.NewWithDatabaseInstance()")) os.Exit(1) } return m } func showUsage() { fmt.Println(` ------------------------------------- Usage: go run migration/main.go <command> Commands: new FILENAME Create new up & down migration files up Apply up migrations down Apply down migrations drop Drop everything version Check current migrate version -------------------------------------`) } func newMigration(name string) { if name == "" { fmt.Println("\nerror: migration file name must be supplied as an argument") os.Exit(1) } base := fmt.Sprintf("./migration/migrations/%s_%s", time.Now().Format("20060102030405"), name) ext := ".sql" createFile(base + ".up" + ext) createFile(base + ".down" + ext) } func createFile(fname string) { if _, err := os.Create(fname); err != nil { panic(err) } } func up(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Up() if err != nil { if err.Error() != "no change" { panic(err) } fmt.Println("\nno change") } else { fmt.Println("\nUpdated:") version, dirty, err := m.Version() showVersionInfo(version, dirty, err) } } func down(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Steps(-1) if err != nil { panic(err) } else { fmt.Println("\nUpdated:") showVersionInfo(m.Version()) } } func drop(m *migrate.Migrate) { err := m.Drop() if err != nil { panic(err) } else { fmt.Println("Dropped all migrations") return } } func showVersionInfo(version uint, dirty bool, err error) { fmt.Println("-------------------") fmt.Println("version : ", version) fmt.Println("dirty : ", dirty) fmt.Println("error : ", err) fmt.Println("-------------------") } マイグレーションのためのsqlファイル 1_articles.down.sql drop table if exists articles 1_articles.up.sql create table if not exists articles ( id integer auto_increment primary key, title varchar(40), description varchar(40) ) 2_users.down.sql drop table if exists users 2_users.up.sql create table if not exists users ( id integer auto_increment primary key, username varchar(40), password varchar(40) ) マイグレーションする dockerコンテナに入ってマイグレーションします。go run main.go upでマイグレーションが行われます。 $ docker exec -it container_name bash # cd migration # go run main.go up start migration Before: ------------------- version : 0 dirty : false error : no migration ------------------- Updated: ------------------- version : 2 dirty : false error : <nil> ------------------- これでdbでもテーブルができているのを確認したらOKです! Sequel Proなどのアプリで確認できます。 PostmanでのAPI確認 これは Postmanというデスクトップアプリで確認することができます。 Post JSON形式で送ります。 GET データベースにも hoge と hello worldが入っているのが確認できました。 コマンドラインでもカラフルに表示されています。 まとめ air + gin + gorm + golang-migrate + mysql + delve + dockerで開発用APIサーバーを立ち上げることができました。 今後試してみたいこと herokuにデプロイしてみる userのところの処理がテキトーなままなので直します、、、 フロントエンド(next + reactなど)とつなげてみる(そもそもレポジトリ名にnextを含んでいたので繋げようと思ったのですが、記事が長すぎてしまったので次回以降やりたいと思います。) 参考 go-ginでサクッとRESTAPIを構築する golang,docker,mysqlの環境をherokuにデプロイする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『ホットリロード x Go言語 x MySQL』なHTTPサーバのDocker環境構築手順

こちらに移動しました。 https://nishinatoshiharu.com/install-go-air/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SQL] LeetCodeの無料問題集を活用して、SQLを使いこなし(4)

Q180 Consecutive Numbers 質問 少なくとも3回連続して表示される数値を検索します。 Code書き方のHint 連続表示される数値を探すので、1行目の数字は2行目、3行目の数字と比較して、一致すれば、その数字は求められるのです。 数字の後ろの値を取得するため、ここはLEAD関数を使います。 REF:https://dev.mysql.com/doc/translation-refman/8.0/ja/functions.html#function_lead LEAD(expr [, N[, default]]) [null_treatment] over_clause LEAD関数: n個後の行を取得する LEAD(列名,n行) over(...) Code SELECT DISTINCT Num AS ConsecutiveNums FROM(SELECT Num, LEAD(Num,1)over()as Num1, LEAD(Num,2)over()as Num2 FROM Logs) T WHERE T.Num=T.Num1 AND T.Num=T.Num2; Result
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語でデータベース(MySQL)に接続する方法

Go言語でデータベースに接続するための方法について紹介します。 今回はMySQLを利用します。 Go言語でデータベースへ接続する手順 データベース接続のパッケージをインポート 接続するデータベースのドライバをインポート データベースのオープン 以下では各手順について紹介します。 データベース接続のパッケージをインポート データベースに接続するためのパッケージをインポートします。 Go言語の標準パッケージではdatabase/sqlが該当します。 import "database/sql" 接続するデータベースのドライバをインポート データベースに接続するにはドライバが必要です。ドライバはデータベースの種類ごとに用意されています。 たとえばMySQLの場合はgithub.com/go-sql-driver/mysqlが該当します。 ドライバのパッケージは直接コード上で利用しないのでアンダースコア(_)でインポートします。(blank identifier) import _ "github.com/go-sql-driver/mysql" 各種データベースのドライバはこちらで確認ができます。 データベースのオープン データベースへアクセスするためのオブジェクトを作成します。 database/sqlの場合、Open関数を利用してdb, err := sql.Open("ドライバ名", "接続文字列")のようにすると変数dbを通じてデータベースへアクセスできます。 "接続文字列"は各種ドライバのドキュメントを参考に記載します。MySQLの場合はこちらににフォーマットが記載されています。 動作確認 実際にGo言語からデータベースに接続してみます。 下準備: 検証用のデータベースをDockerで用意する 今回はMySQLのDockerイメージを利用して検証用のデータベースを構築します。 データベース接続情報 ユーザー名: webuser パスワード: webpass データベース名: go_mysql8_development ポート: 3306 docker-compose.yml version: '3' services: db: image: mysql:8.0.21 ports: - '3306:3306' command: --default-authentication-plugin=mysql_native_password environment: MYSQL_USER: 'webuser' MYSQL_PASSWORD: 'webpass' MYSQL_ROOT_PASSWORD: 'pass' MYSQL_DATABASE: 'go_mysql8_development' docker-compose.ymlでコンテナ起動後、以下のようにデータベースに接続できればOKです。 ### コンテナの起動 $ cd /path/to/docker-compose-file $ docker-compose up -d ### dbコンテナにアクセスし、データベースがあることを確認 $ docker-compose exec db mysql -uwebuser -pwebpass -D go_mysql8_development -e "show databases" +-----------------------+ | Database | +-----------------------+ | go_mysql8_development | | information_schema | +-----------------------+ サンプルコードの実装と実行 最終的なサンプルコードは以下のようになります。 sql.Openはあくまでデータベースへ接続するオブジェクトを生成しているだけです。 ですので、実際のデータベースへの接続テストはdb.Ping()で確認します。 main.go package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func main() { // [ユーザ名]:[パスワード]@tcp([ホスト名]:[ポート番号])/[データベース名]?charset=[文字コード] dbconf := "webuser:webpass@tcp(127.0.0.1:3306)/go_mysql8_development?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) // 接続が終了したらクローズする defer db.Close() if err != nil { fmt.Println(err.Error()) } err = db.Ping() if err != nil { fmt.Println("データベース接続失敗") return } else { fmt.Println("データベース接続成功") } } サンプルコードを実行し、以下のように『データベース接続成功』の文字列が表示されればOKです。 ### 作業ディレクトリへ移動 $ cd /path/to/project ### パッケージのインストール $ go mod init example $ go mod tidy ### 実行 $ go run main.go データベース接続成功 参考: データベース接続のソースコードをリファクタリングする 上記で紹介したソースコードは以下の改善点があります。 データベース接続のロジックがmain関数に直接書かれている データベース情報がソースコードにベタ書きされている ここからはソースコードのリファクタリング方法について紹介します。 データベース接続のロジックを別パッケージ化する データベース接続のロジックを別パッケージにすることでmain関数をシンプルにします。 main.go package main import ( + "example/database" // "モジュール名/パッケージ名" "fmt" - "database/sql" - _ "github.com/go-sql-driver/mysql" ) func main() { + db := database.Connect() - dbconf := "webuser:webpass@tcp(127.0.0.1:3306)/go_mysql8_development?charset=utf8mb4" - - db, err := sql.Open("mysql", dbconf) - if err != nil { - fmt.Println(err.Error()) - } defer db.Close() err := db.Ping() if err != nil { fmt.Println("データベース接続失敗") return } else { fmt.Println("データベース接続成功") } } データベース接続に関するパッケージは以下の通りです。 外部パッケージから呼び出せるようにするため、関数名は大文字から始めます。 database/connect.go package database import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { user := "webuser" password := "webpass" host := "localhost" port := "3306" database_name := "go_mysql8_development" dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } GoDotEnvを導入してデータベース接続情報を.envファイルに配置する GoDotEnvはGo言語で.envファイルを利用できるようにするパッケージです。 GoDotEnvを利用することでデータベース接続情報を環境変数に置き換えられます。 GoDotEnvはgodotenv.Load()で環境変数をロードし、os.Getenv("Key名")で値を呼び出します。 GoDotEnvを利用したdatabaseパッケージのソースコードは以下の通りです。 .env DB_USER=webuser DB_PASSWORD=webpass DB_HOST=db DB_PORT=3306 DB_DATABASE_NAME=go_mysql8_development database/connect.go package database import ( "database/sql" "fmt" "os" "github.com/joho/godotenv" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { err := godotenv.Load() if err != nil { fmt.Println(err.Error()) } user := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") database_name := os.Getenv("DB_DATABASE_NAME") dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } 最終的なソースコード リファクタリングを終えた最終的なソースコードは以下の通りです。 main.go package main import ( "example/database" "fmt" ) func main() { db := database.Connect() defer db.Close() err := db.Ping() if err != nil { fmt.Println("データベース接続失敗") return } else { fmt.Println("データベース接続成功") } } database/connect.go package database import ( "database/sql" "fmt" "os" "github.com/joho/godotenv" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { err := godotenv.Load() if err != nil { fmt.Println(err.Error()) } user := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") database_name := os.Getenv("DB_DATABASE_NAME") dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } .env DB_USER=webuser DB_PASSWORD=webpass DB_HOST=db DB_PORT=3306 DB_DATABASE_NAME=go_mysql8_development まとめ Go言語でデータベースに接続する方法 データベース接続のパッケージをインポート 接続するデータベースのドライバをインポート データベース接続情報をドライバのパラメータに渡す データベースをオープンし、オブジェクトを生成 オブジェクトを通じてデータベースを操作する 認識違いや補足があればコメントいただけると助かります。 参考 プログラミング言語Go完全入門: 11. データベース 【Go実践】GoでMySQLを使おう(1) – ドライバのインストールからデータベース接続まで Go database/sql の操作ガイドあったんかい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む