- 投稿日:2021-01-19T21:37:12+09:00
性能測定の準備をまとめてみました
はじめに
最近始まったプロジェクトの中で、開発しているSaaSのアプリケーションの性能測定を行うことになりました。
自分の頭の中を整理するためにも性能測定でやることをまとめました。
これから「準備」「計測」「報告」の3stepに分けて、記事を配信していこうと考えています。
第一回目は「性能測定の準備」についてです。※間違いや不明点などありましたらご指摘いただけると幸いです。
事前知識と背景
なぜ性能測定は必要なのか?
ソフトウェアを開発するときには、それが何をするのか=機能、それがどのように動作するのか=非機能を決めることが求められます。そして、ソフトウェアの品質を高めるためには、機能、非機能の観点で要件通りに動作するのかをテストする必要があります。
機能テストは注意深く行われますが、非機能テストが行われないケースはしばしば見受けられます。
以下は、ISOによって定められた製品品質モデルと呼ばれるもので、検証すべき品質特性の分類です。
品質特性 概要 機能適合性 明示された状況下で使用するとき、明示的ニーズ及び暗黙のニーズを満足させる機能を、製品又はシステムが提供する度合い。 性能適合性 明記された状態(条件)で使用する資源の量に関係する性能の度合い。 互換性 同じハードウェア環境又はソフトウェア環境を共有する間、製品、システム又は構成要素が他の製品、システム又は構成要素の情報を交換することができる度合い、及び/又はその要求された機能を実行することができる度合い。 使用性 明示された利用状況において、有効性、効率性及び満足性をもって明示された目標を達成するために、明示された利用者が製品又はシステムを利用することができる度合い。 信頼性 明示された時間帯で、明示された条件下に、システム、製品又は構成要素が明示された機能を実行する度合い。 セキュリティ 人間又は他の製品若しくはシステムが、認められた権限の種類及び水準に応じたデータアクセスの度合いをもてるように、製品又はシステムが情報及びデータを保護する度合い。 保守性 意図した保守者によって、製品又はシステムが修正することができる有効性及び効率性の度合い。 移植性 一つのハードウェア、ソフトウェア又は他の運用環境若しくは利用環境からその他の環境に、システム、製品又は構成要素を移すことができる有効性及び効率性の度合い。 (ISO/IEC 25010:2011)
様々にありますが、この記事では「性能適合性」の品質を検証するための、性能テストについて述べていきます。
システムの性能とは何か?
性能について調べてみると、下記のように表現されていました。
性能とは、「システムが処理結果を返す力」です。
https://gihyo.jp/dev/serial/01/tech_station/0008(他にも書かれていましたが、なんとなくこちらがしっくりきたので、、、)
性能=「処理結果を返す力」の要因が大きく以下の3つです。
レスポンスタイム = 処理の応答時間
スループット = 時間あたりの処理能力
リソース使用量 = 処理を行うための必要な資源最終的にこれらの測定を行い、性能的に問題ないかを見ていくことがゴールになります。
それぞれの測定対象、測定方法は以下を用います。
性能の指標 測定対象 測定方法 レスポンスタイム api、画面のレスポンス速度 Jmeter、devtool スループット 複数スレッドの時間あたりのapiの処理件数 Jmeter リソース使用量 CPU、ディスク、メモリ、ネットワーク AWSのモニタリング ※スループットについて
https://codezine.jp/article/detail/9614背景
今回、性能測定を行うに至った理由は、開発しているアプリケーションの機能要件を大きく変更することになったからです。この機能要件の変更により、大量データの作成、取得を行う必要があるため、非機能要件を満たすかを検証する必要がありました。
この性能検証の結果で非機能要件を満たせなければ、ユーザーにとっては嬉しいであろう機能要件も変更せざるを得なくなります。そのため、今回の結果がプロジェクトの生死を分かつとっていっても過言ではありません。性能測定の準備
大きな流れ
筆者は最初、性能測定をするとなったので、
「よし、早速apiのレスポンス速度を測ってみよう」
などと思っていましたが、そう簡単なことではありませんでした。
性能測定の序盤ではこんなやり取りもありました。先輩
「そもそも、測定対象何かわかっている?」
「そもそも、機能要件を満たすための大量データがある状態で、測定を行う必要あるけど、その大量データってどんなデータがどのくらい必要かわかっている?」
「そもそも、まだ機能自体完成していないから、データなんてないよ。データ作成する必要あるけど、データの作成の仕方わかっている?」
筆者
「どれも、どうやるのか全然わからないです。」整理すると、
性能測定をするためには、機能要件を満たした場合、
どんなデータが必要かを調べて、そのデータを作成して、そのデータによって影響のある対象を測定する必要があります。
それぞれの工程とそのアウトプット物は以下になります。
工程 アウトプット物 データ要件定義 データ要件定義書 データ作成 データ作成用のスクリプト、データ作成手順書 測定対象のスコープ決め 測定対象一覧シート やることが少しクリアになったところでこれからそれぞれの工程を詳しくみていきます。
データ要件定義
まず、どんなデータが必要かを考えていきます。
ここでのアウトプットは、データ要件定義書です。
今回性能測定するソフトウェアは、Saasプロダクトのため、企業単位でどんなデータが必要かを決めていきます。
データ要件定義の中では、やることは大きく以下の2つです。・データ定義する項目を決める(ex.ユーザー数)
・データ量がどのくらいを決める(ex.ユーザー数:1万人)データ定義する項目を決める
データ定義が必要な項目は、今回利用するデータと依存関係のあるデータ項目です。
例えば、企業の地域あたりのユーザー数が利用するデータの場合、そのデータと依存関係のあるユーザー数と地域数という項目のデータ量を定義する必要があります。データ量がどのくらいを決める
データを定義する項目が決まると、項目ごとのデータ量を決める必要があります。
データ量を決める上で大事なのが想定される最大のデータ量を考えることです。
何年後までを想定した仕様にするか、その時の最大規模の企業での項目ごとのデータ量はどの程度か(企業数も含めて)によりデータ量が変わります。
これは、事業戦略として何年後にどのくらいの規模の企業にプロダクトを納品するか、といったビジネスサイドの話が大きく関わってきます。データの作成
必要なデータが分かったら、次にそのデータを作成していきます。
ここでのアウトプットは、データ作成手順書、データ作成用のスクリプトになります。今回変更する機能は、まだできていないため利用するデータを作成する必要があります。
データの作成の中でやることは、大きく以下の2つです。・実際に測定を行うときに必要なデータを作成するプロセスを手順化する。
・データを作成する方法を明らかにし、必要であればスクリプトを作成する。データ作成手順書
下記のようなデータ作成手順書を作成し、データ要件定義で決めたデータを作成する上で何が必要かを考えます。
no 工程 1 測定環境の構築 2 ユーザーデータの作成 3 地域データの作成 4 地域あたりのユーザーの集計 データ作成方法
データを作成する工程については、大きく下記の方法で実現していきます。
・アプリケーション側からデータを設定する
・スクリプトを作成する今後の測定も考えた上で一番早い方法を選択し、必要があればスクリプトを新たに作成して、データを作成します。
測定対象のスコープ決め
必要なデータが準備できたら、測定対象のスコープを決めていきます。
ここでのアウトプットは、測定対象の一覧になります。
測定対象は大きく下記の2つがあるので、分けて測定対象のスコープを決めます。・アプリケーション側で呼ばれる画面、api
・非同期で実行されるバッチ処理画面、api
画面、apiに関しては、今回大量作成されるデータに関する値が返ってくるものを測定対象とします。
スコープの特定はdevtoolを用いて行います。バッチ処理
今回のアプリケーションでは、重めの処理を非同期でアプリケーションサーバーとは別のサーバーで実行しています。
api同様に、今回大量作成するデータに対して、CRUD操作を行うものに絞って、測定対象のスコープを決めます。
スコープの特定はソースコードから行います。まとめ
今回は、性能測定するまでの準備をまとめてみました。
非機能要件を満たしていなかったら、機能要件も見直す必要もあるので、新たに機能を追加する上で性能検証はすごい重要なことだと感じました。
自分でアウトプットしようとすると、怪しい点がいくつも出てきたので、よかったです。
次は具体的に性能測定方法などを書こうと思います。まだまだ理解が不足していることもあると思うので、ご意見、FBあればお願いします!
- 投稿日:2021-01-19T21:35:05+09:00
外部キーを持つデータの削除でdependentオプションが効かず焦った
開発環境
Mac OS Catalina 10.15.7
ruby 2.6系
rails 6.0系各モデルのアソシエーション
user.rbhas_many :posts has_many :commentspost.rbbelongs_to :user has_many :commentscomment.rbbelongs_to :user belongs_to :postエラー内容
ActiveRecord::InvalidForeignKey in PostsController#destroy Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint failsこのエラー自体は過去にも見たことがあったので、「あ〜はいはい、外部キーを持つデータの削除だから、dependentオプション付けなきゃダメなんでしょ」とdependentオプションを付けて再度削除を試みました。
user.rbhas_many :posts has_many :comments, dependent: :destroypost.rbbelongs_to :user has_many :comments, dependent: :destroyしかし、エラーは解決せず、、、
解決法
なかなか原因がわからなかったので、関連ファイルをあたってみたところ、マイグレーションファイル内に怪しいコードを発見しました。
エラー時のコード
class CreateComments < ActiveRecord::Migration[6.0] def change create_table :comments do |t| t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true t.string :content, null: false t.timestamps end end end
解決後のコード
class CreateComments < ActiveRecord::Migration[6.0] def change create_table :comments do |t| t.references :user, foreign_key: true t.references :post, foreign_key: true t.string :content, null: false t.timestamps end end end
結論マイグレーションファイル内に記述していたnull:falseがよくなかったみたいです。
まとめ
正直にいうと、原因を的確には説明できないのですが、過去にアプリケーションを作成したときは外部キーにnull:falseは付けてなかったなと思い、変更してみたらうまくいきました。
まだまだ、理解しているようで理解できていないことが多いですね。説明できる方いましたら、教えていただけると幸いです。
- 投稿日:2021-01-19T20:07:31+09:00
Go + MySQL + nginxの開発環境をDocker(docker-compose)で作る
やりたいこと
- Go、MySQL、nginxの開発環境をDocker(docker-compose)で作る
- Goの外部パッケージはGo Modulesで管理
- Goのプロジェクトを実践的なものにする
- DBのテーブル管理はマイグレーションを使う
- testはテスト用のDBを使う
こんな人におすすめ
- GoとMySQLでAPIサーバーの開発がしたい
- 環境構築はDockerで手っ取り早く済ませたい
- 拡張しやすいGoのプロジェクトが欲しい
使用するフレームワーク、バージョン
バージョン Go 1.15 MySQL 5.7 nginx 1.19 ディレクトリ構成
├── docker-compose.yml ├── Dockerfile ├── app │ ├── cmd │ │ ├── migrate │ │ │ └── main.go │ │ └── server │ │ └── main.go │ ├── db │ │ └── migrations │ │ ├── 1_create_users.down.sql │ │ └── 1_create_users.up.sql │ ├── go.mod │ ├── go.sum │ └── pkg │ ├── connecter │ │ └── connecter.go │ ├── controller │ │ ├── router.go │ │ └── users.go │ └── model │ ├── main_test.go │ ├── user.go │ └── user_test.go ├── mysql │ ├── Dockerfile │ ├── docker-entrypoint-initdb.d │ │ └── init.sql │ └── my.cnf └── nginx ├── Dockerfile └── default.confルートディレクトリにあるDockerfileがGoのコンテナ用です。
使い方
GitHubレポジトリはこちらにあります。
https://github.com/fuhiz/docker-go-sampleまずはdocker-compose.ymlがあるディレクトリでコンテナを立ち上げます。
$ docker-compose up -d --buildGoのコンテナに入ります。
$ docker-compose exec web bashGoのコンテナの/appでマイグレーションを実行します。
実行されるSQLはapp/db/migrationsのファイルです。
usersとマイグレーション管理のためのschema_migrationsが作られます。$ go run cmd/migrate/main.go -exec upusersには名前(name)、年齢(age)、日時カラムを用意しました。
/db/migrations/1_create_users.up.sqlCREATE TABLE `users` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `age` int NOT NULL, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL, `deleted_at` datetime, PRIMARY KEY (`id`) ) DEFAULT CHARSET=utf8mb4;サーバーを起動します。
$ go run cmd/server/main.goこれでlocalhost:8082でGoのAPIにつながるようになります。
APIを使ってみる
DBにはusersテーブルがあって、ユーザーのCRUD機能が使えるようになっているので、curlで確認します。
- ユーザー作成
$ curl localhost:8082/api/v1/users -X POST -H "Content-Type: application/json" -d '{"name": "test", "age":30}'
- ユーザー一覧
$ curl localhost:8082/api/v1/users {"users":[{"ID":1,"CreatedAt":"2021-01-09T11:09:31+09:00","UpdatedAt":"2021-01-09T11:09:31+09:00","DeletedAt":null,"name":"test","age":30}]}%先ほど作ったユーザーが取得できます。
- ユーザー更新
$ curl localhost:8082/api/v1/users/1 -X PATCH -H "Content-Type: application/json" -d '{"name": "update", "age":31}'
- ユーザー削除
$ curl localhost:8082/api/v1/users/1 -X DELETEdocker-compose.yml
ここから環境構築の細かいところを見ていきます。
docker-compose.ymlの基本的な書き方には触れないので、参考にされる方はこちらを。
https://qiita.com/zembutsu/items/9e9d80e05e36e882caaaそれぞれのserviceについてはこのようになっております。
db
- MySQLコンテナ
- ユーザー名やパスワードなどを環境変数で定義。
- docker-entrypoint-initdb.dをマウントして、コンテナ起動時にdocker-entrypoint-initdb.d/init.sqlが実行されるようにします。init.sqlでgo_sampleとgo_sample_testというデータベースを作ります。 /docker-entrypoint-initdb.dはMySQLのDockerイメージに備わっているディレクトリで初期データを作ることができます。
- ホスト側のポートが3310なのはローカルで動かすMySQLと被らないようにするためです。
- Sequel Proで接続するときはこうなります。
※パスワードはlocalpass。データベースは空でも構わないです。web
- Goコンテナ
- 起動後すぐにコンテナが閉じてしまわないようにtty: trueでコンテナを永続化します。 (サーバー起動をDockerfileに書かず、コンテナの中で手動で実行するためです)
- Goプロジェクト内で使う環境変数を定義。Goのコードで
os.Getenv("DB_PASSWORD")
とすればこの値が読み込めます。DB_HOSTのdbはMySQLコンテナのサービス名です。GORMでDB接続するときにこのサービス名で接続できます。- Goプロジェクトがある./app(ホスト)を/app(コンテナ)にマウントします。コンテナ内の/appはDockerfileのWORKDIRで指定したときに作成されます。
proxy
- nginxはリバースプロキシによってURLを転送します。ここでは
http://localhost
がGoのAPIになるように設定する目的で使います。- ホスト側のportは8082を指定しました。
docker-compose.ymlversion: "3" services: db: build: ./mysql environment: MYSQL_ROOT_PASSWORD: root MYSQL_USER: localuser MYSQL_PASSWORD: localpass TZ: Asia/Tokyo volumes: - ./mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d ports: - "3310:3306" web: build: . tty: true environment: APP_MODE: local DB_PASSWORD: localpass volumes: - "./go:/app" depends_on: - db proxy: build: ./nginx ports: - 8082:80 depends_on: - webMySQLコンテナ
MySQLのDockerfileはこれらの一般的な設定です。
タイムゾーンをAsia/Tokyoにする。
設定ファイルのmy.cnfをコピーする。
起動時に実行するinit.sqlをコピーする。mysql/DockerfileFROM mysql:5.7 ENV TZ Asia/Tokyo RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && chown -R mysql:root /var/lib/mysql/ COPY my.cnf /etc/mysql/conf.d/my.cnf COPY docker-entrypoint-initdb.d/init.sql /docker-entrypoint-initdb.d/ CMD ["mysqld"] EXPOSE 3306init.sqlではgo_sampleとテスト用のgo_sample_testを作ってユーザーの権限設定をします。
mysql/docker-entrypoint-initdb.d/init.sqlCREATE DATABASE IF NOT EXISTS `go_sample` COLLATE 'utf8mb4_general_ci' ; CREATE DATABASE IF NOT EXISTS `go_sample_test` COLLATE 'utf8mb4_general_ci' ; GRANT ALL ON `go_sample`.* TO 'localuser'@'%' ; GRANT ALL ON `go_sample_test`.* TO 'localuser'@'%' ; FLUSH PRIVILEGES ;Goコンテナ
内容はコメントの通りで、外部パッケージをダウンロードするためにgo.modとgo.sumを事前にコピーしています。
DockerfileFROM golang:1.15 ## 作業ディレクトリ WORKDIR /app # モジュール管理のファイルをコピー COPY go/go.mod . COPY go/go.sum . # 外部パッケージのダウンロード RUN go mod download EXPOSE 9000nginxコンテナ
nginx.confで読み込むdefault.confをコピーします。
nginx/DockerfileFROM nginx:1.19-alpine COPY ./default.conf /etc/nginx/conf.d/default.conf EXPOSE 80nginx/default.confserver { listen 80; server_name localhost; location / { proxy_pass http://web:9000; } }nginxの設定ファイルである/etc/nginx/nginx.confで/etc/nginx/conf.d/配下の*.confを読み込むようになっているので、読み込まれる部分だけを作っています。
nginxの設定はこちらが参考になります。
https://qiita.com/morrr/items/7c97f0d2e46f7a8ec967大事なのは
proxy_pass http://web:9000;
の部分で、ここでhttp://localhost
をhttp://web:9000
に置き換えています。
webはdocker-compose.ymlで定義したGoコンテナのサービス名です。
docker-composeはサービス間でネットワーク通信できるので、このような指定ができます。
Goプロジェクトはポートを9000でサーバーを立ち上げているのでポートはそれに合わせます。また、docker-compose.ymlのnginxコンテナでポートを8082:80としているので、ホストからは
http://localhost:8082
でアクセスします。ややこしいですが、とどのつまりは
http://localhost:8082
でGoのAPIが叩けることになります。Goのプロジェクト概要
Goのコードはなるべく実践的に使えるものを意識して作りました。
ディレクトリ構造はこちらを参考にしています。
https://qiita.com/sueken/items/87093e5941bfbc09bea8cmd
アプリケーションのエントリーポイント。
サーバー起動とマイグレーション機能を配置。db
マイグレーションで実行したいsqlファイルを配置。pkg
アプリケーションの挙動に関わる部分。
モデル(model)、コントローラー(controller)、接続(connecter)を作成。マイグレーション
マイグレーション周りはこちらを参考にさせていただきました。
https://qiita.com/tanden/items/7b4fb1686a61dd5f580dgolang-migrateを使用して、db/migrationsにあるsqlファイルでDBを管理します。
ファイル名のルールは{version}を昇順にすれば、番号でもタイムスタンプでも問題ありません。
https://github.com/golang-migrate/migrate/blob/master/MIGRATIONS.md{version}_{title}.up.{extension} {version}_{title}.down.{extension}ここではusersテーブルを作成する
1_create_users.up.sql
とテーブル削除用の1_create_users.down.sql
を作成しました。マイグレーション管理のファイルは
cmd/migrate/main.go
にあります。内容は参考サイトのほぼコピペになります。このファイルを実行すれば追加した分の*.up.sqlだけが走ります。
$ go run cmd/migrate/main.go -exec up戻したいときはオプションをdownにすれば、全ての*.down.sqlが実行されます。
$ go run cmd/migrate/main.go -exec downtest用のデータベースに接続したいときはAPP_MODE=testで環境変数つきで実行します。
$ APP_MODE=test go run cmd/migrate/main.go -exec upcmd/migrate/main.goのinit()でAPP_MODEがtestなら、データベースはDB_NAME_TESTを使うようにしてます。
cmd/migrate/main.gofunc init() { // database name decide by APP_MODE dbName := os.Getenv("DB_NAME") if os.Getenv("APP_MODE") == "test"{ dbName = os.Getenv("DB_NAME_TEST") } Database = fmt.Sprintf("mysql://%s:%s@tcp(%s:%s)/%s", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), dbName) }GoのAPIの処理の流れ
エントリーポイントとなるファイルはcmd/server/main.go。
ginを使って、ポート9000でサ-バーを立ち上げています。cmd/server/main.gopackage main import ( "net/http" "github.com/gin-gonic/gin" "github.com/fuhiz/docker-go-sample/app/pkg/connecter" "github.com/fuhiz/docker-go-sample/app/pkg/controller" ) func main() { // gormのDB接続 connecter.Setup() router := gin.Default() // apiの疎通確認用 router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "Response OK") }) // routing r := router.Group("/api/v1") controller.Setup(r) router.Run(":9000") }細かい処理は/pkgのcontrollerなどを使っていきます。
gormのDB接続はpkg/connecter/connecter.goで行います。
変数dbに*gorm.DBを格納して、DB()で呼び出せる形になっています。接続の仕方は公式を見れば大体把握できます。
https://gorm.io/docs/connecting_to_the_database.htmlデータベースの各パラメータはdocker-compose.ymlで定めた環境変数から取得しています。
ここでもAPP_MODEがtestならDB_NAME_TESTを使います。pkg/connecter/connecter.gopackage connecter import ( "fmt" "os" "gorm.io/driver/mysql" "gorm.io/gorm" ) var db *gorm.DB func Setup() { // APP_MODEからデータベース名を決める dbName := os.Getenv("DB_NAME") if os.Getenv("APP_MODE") == "test"{ dbName = os.Getenv("DB_NAME_TEST") } // DB接続 (https://gorm.io/docs/connecting_to_the_database.html) dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), dbName, os.Getenv("DB_LOC")) gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } db = gormDB } func DB() *gorm.DB { return db }ルーティングはpkg/controllers/router.goに書きます。
それぞれpkg/controllers/users.goのfuncを呼びます。pkg/controllers/router.gopackage controller import ( "github.com/gin-gonic/gin" ) func Setup(r *gin.RouterGroup) { users := r.Group("/users") { u := UserController{} users.GET("", u.Index) users.GET("/:id", u.GetUser) users.POST("", u.CreateUser) users.PATCH("/:id", u.UpdateUser) users.DELETE("/:id", u.DeleteUser) } }pkg/controllers/users.goでは処理に応じて/pkg/model/user.goのfuncを呼びます。
pkg/controllers/users.gopackage controller import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/fuhiz/docker-go-sample/app/pkg/connecter" "github.com/fuhiz/docker-go-sample/app/pkg/model" ) type UserController struct{} type UserParam struct { Name string `json:"name" binding:"required,min=1,max=50"` Age int `json:"age" binding:"required,number"` } // ユーザー取得 func (self *UserController) GetUser(c *gin.Context) { ID := c.Params.ByName("id") userID, _ := strconv.Atoi(ID) user, err := model.GetUserById(connecter.DB(), userID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"user": user}) } // ユーザー一覧 func (self *UserController) Index(c *gin.Context) { users, err := model.GetUsers(connecter.DB()) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user search failed"}) return } c.JSON(http.StatusOK, gin.H{"users": users}) } // ユーザー作成 func (self *UserController) CreateUser(c *gin.Context) { var param UserParam if err := c.BindJSON(¶m); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } newUser := model.NewUser(param.Name, param.Age) user, err := model.CreateUser(connecter.DB(), newUser) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user create failed"}) return } c.JSON(http.StatusOK, gin.H{"user": user}) } // ユーザー更新 func (self *UserController) UpdateUser(c *gin.Context) { ID := c.Params.ByName("id") userID, _ := strconv.Atoi(ID) user, err := model.GetUserById(connecter.DB(), userID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"}) return } var param UserParam if err := c.BindJSON(¶m); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } updateParam := map[string]interface{}{ "name": param.Name, "age": param.Age, } _, err = user.Update(connecter.DB(), updateParam) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user update failed"}) return } c.JSON(http.StatusOK, gin.H{"user": user}) } // ユーザー削除 func (self *UserController) DeleteUser(c *gin.Context) { ID := c.Params.ByName("id") userID, _ := strconv.Atoi(ID) user, err := model.GetUserById(connecter.DB(), userID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"}) return } _, err = user.Delete(connecter.DB()) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user delete failed"}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) }/pkg/model/user.gopackage model import ( "gorm.io/gorm" ) type User struct { gorm.Model Name string `json:"name"` Age int `json:"age"` } func NewUser(name string, age int) *User { return &User{ Name: name, Age: age} } func CreateUser(db *gorm.DB, user *User) (*User, error) { result := db.Create(&user) return user, result.Error } func GetUsers(db *gorm.DB) ([]*User, error) { users := []*User{} result := db.Find(&users) return users, result.Error } func GetUserById(db *gorm.DB, ID int) (*User, error) { user := User{} result := db.First(&user, ID) return &user, result.Error } func (user *User) Update(db *gorm.DB, param map[string]interface{}) (*User, error) { result := db.Model(&user).Updates(param) return user, result.Error } func (user *User) Delete(db *gorm.DB) (*User, error) { result := db.Delete(&user) return user, result.Error }テスト
テストはGoのコンテナ内でAPP_MODE=testをつけて実行します。
以下手順。マイグレーション(up)でgo_sample_testにテーブルを作成。
$ APP_MODE=test go run cmd/migrate/main.go -exec up/pkgをテスト。
$ APP_MODE=test go test -v ./pkg/...次のテストのためにgo_sample_testを戻す。
$ APP_MODE=test go run cmd/migrate/main.go -exec downテストファイルは/pkg/modelにmain_test.goとuser_test.goがあります。
TestMainが最初に実行されるので、そこでDB接続しときます。/pkg/model/main_test.gopackage model_test import ( "testing" "github.com/fuhiz/docker-go-sample/app/pkg/connecter" ) func TestMain(m *testing.M) { connecter.Setup() m.Run() }ユーザー作成のテスト。
/pkg/model/user_test.gopackage model_test import ( "testing" "github.com/fuhiz/docker-go-sample/app/pkg/connecter" "github.com/fuhiz/docker-go-sample/app/pkg/model" ) func TestCreateUser(t *testing.T) { newUser := model.NewUser("test_user", 30) user, _ := model.CreateUser(connecter.DB(), newUser) if user.Name != "test_user" { t.Fatal("model.CreateUser Failed") } }まとめ
ローカル環境としてはそれなりに使える環境が整えられたと思います。
自動テストやデプロイにも対応できるかは今後検証していきたいです。Goはまだまだベストプレクティスが確立されていないようでテスト環境の切り分けは苦労しました。
改めてLaravelやRailsのような全部入りのフレームワークの偉大さも感じました。長めの記事でしたが参考にしてもらえたらありがたいです!
- 投稿日:2021-01-19T19:51:54+09:00
Rails6[APIモード]+ MySQL5.7 を Docker で環境構築
はじめに
自分用です
Rails6 APIモード + MySQL5.7 を Docker(docker compose) で環境構築Dockerそのものの導入は省略
はじめに環境構築に必要なファイルを作成
- 以下のファイルを作成する
- アプリ用のトップレベルディレクトリ
- Dockerfile
- docker-compose.yml
- Gemfile
- Gemfile.lock
アプリ用のトップレベルディレクトリ作成&移動
$ cd $ mkdir sample_app $ cd sample_app
Dockerfile, docker-compose.yml, Gemfile, Gemfile.lock作成
sample_app$ touch {Dockerfile,docker-compose.yml,Gemfile,Gemfile.lock} sample_app$ ls Dockerfile docker-compose.yml Gemfile Gemfile.lock
ファイルの中身書いていく
sample_app/Dockerfileファイル
DockerfileFROM ruby:2.6.5 # 必要なパッケージのインストール(Rails6からWebpackerがいるので、yarnをインストールする) RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && apt-get update -qq \ && apt-get install -y build-essential libpq-dev nodejs yarn # 作業ディレクトリの作成 RUN mkdir /myapp WORKDIR /myapp # ホスト側(ローカル)(左側)のGemfileを、コンテナ側(右側)のGemfileへ追加 ADD ./Gemfile /myapp/Gemfile ADD ./Gemfile.lock /myapp/Gemfile.lock # Gemfileのbundle install RUN bundle install ADD . /myapp
sample_app/docker-compose.yml
docker-compose.ymlversion: '3' services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: root ports: - "3306:3306" web: build: . command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3001 -b '0.0.0.0'" tty: true stdin_open: true depends_on: - db ports: - "3001:3001" volumes: - .:/myapp
sample_app/Gemfile
Gemfilesource 'https://rubygems.org' gem 'rails', '~> 6.0.3'
Gemfile.lockは空のまま
docker-compose run コマンドで Rails アプリを作成
- APIモードなので
--api
オプション付与- バージョン6以降なので、
--webpacker
オプション付与$ docker-compose run web rails new . --force --database=mysql --skip-bundle --api --webpacker
database.yml ファイルを修正
- sample_app/config/database.yml ファイルに、コンテナに作成されたDB情報を記述する
database.ymldefault: &dafault adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password # docker-compose.ymlの MYSQL_ROOT_PASSWORD host: db # docker-compose.ymlの service名
Dockerコンテナを起動する
- コンテナの実行
$ docker-compose build
- コンテナを起動
-d
オプション付与でバックグラウンド実行をする。これを実行すると、コンテナを起動したままプロンプト画面へ戻る$ docker-compose up -d
DBを作成する
- まだコンテナを起動していない場合はしておく
$ docker-compose up -d
- コンテナID確認
$ docker ps -a
- 確認したIDより、コンテナに入る
$ docker exec -it <コンテナのID> /bin/bash
- DB作成
$ rails db:create $ rails db:migrate
- コンテナから出る
$ exit
- コンテナに入らず、ローカルから実行する場合。コンテナ起動後に、
docker-compose run
コマンドを実行する$ docker-compose run web rails db:create $ docker-compose run web rails db:migrate
構築は以上。
localhost:3001
で開くようになった。
その他
サーバーを止める場合
- Ctrl + C で止めないこと。コンテナが残って次回起動時にエラーが出る
- もしやってしまった場合、tmp/pids/server.pid を削除して、再びdocker-compose upで再起動する
$ docker-compose down
Dockerfileやdocker-compose.ymlの変更を反映、railsサーバー再起動
$ docker-compose up --build
bundle install などのコマンドを実行したい場合
# docker-compose run { サービス名 } { 任意のコマンド } $ docker-compose run web bundle install
ローカルからMySQLコンテナに接続
- コンテナ起動してない場合は起動
$ docker-compose up -d
- mysqlのidを確認
$ docker ps
- MySQLコンテナにログイン
$ docker exec -it <MySQLのコンテナのID> /bin/bash
$ mysql -u root -p -h 0.0.0.0 -P 3306 --protocol=tcp mysql> // 脱出 mysql> quit
以上。
参考にさせて頂いた記事
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
【Rails】Rails 6.0 x Docker x MySQLで環境構築
- 投稿日:2021-01-19T16:35:52+09:00
MySQL workbenchでレコードを削除したらjavax.persistence.EntityNotFoundExceptionが発生
環境
Spring Boot 2.4.0
java 1.8
thymeleaf 3.0.11
Spring Security 2.4.0
MySQL 8.0.22現象
MySQLWorkbenchで「いらんなぁ」って思ったレコードを消去しました。
Delete row
でぽんと消しちゃったんですね。そしたらシステムにログインできなくなりました。
別にしようとしてたログインとは関係ないレコードなんですけどね。
また元に戻せば大丈夫ですが、AutoIncrementがあるので、
1. AutoIncrementを外す
2. idを指定するinsertする
3. AutoIncrementをつけるってめんどくさいことをしないと戻せない。
前にもやったんですけど、解決策がわからず前に進まなかったので、とりあえずめんどくさいけど上のやり方をしました。
でもまたあとから見て不要だと思ったので削除しようってやったら、「またかよ・・・」
これからもあるかもしれないので、ちょっと調べました。
解決策
https://stackoverflow.com/questions/46612968/hibernate-unable-to-find-entity-with-id
これを参考にしました。@NotFound(action = NotFoundAction.IGNORE)どうやらこれを追加すれば良いみたい。
どこに?!
@ManyToOne(の下)例
@Entity Supplier { // フィールド省略 } @Entity Wedding { // フィールド省略 @ManyToOne // ←これが消したレコードを探しにいってるみたい・・・ private Supplier supplier; }変更後
@Entity Wedding { // フィールド省略 @ManyToOne @NotFound(action = NotFoundAction.IGNORE) // ←これのおかげで無視してくれるみたい private Supplier supplier; }原因
まだわかりません。
どうして消したはずのレコードのIDを探しにいってるの?
てかなんで覚えてるの?
どこに保存されているの?ということがわかりません。
でもどっかに保存されているのでしょう。
Hibernateってやつですかね??そのあたりSpringBootのことをしっかり理解していないですが、使いながら理解していきたい!!
とりあえず同じ様なエラーで前に進まない方の参考になればと思います。
- 投稿日:2021-01-19T15:01:05+09:00
M1 MAC(Apple Silicon) でmysqlイメージを動かす
docker-compose.yml
に以下の様にplatformを設定するservices: db: platform: linux/x86_64 image: mysql:5.7 ...なんでこれで動くのか不思議だったが、
https://www.publickey1.jp/blog/20/apple_m1docker_desktopcpux86docker.html
によると、
「Docker Desktop for Mac」はマルチCPUアーキテクチャ対応しており、上記の指定だと、
linux/x86_64用のイメージをエミュレーションして動作させることになるっぽい。
エミュレーションするため遅くはなるかもしれない
- 投稿日:2021-01-19T14:31:33+09:00
PHP Portfolio
はじめに
らむです。
今回は、初めてQiitaで投稿します。
理由としては、ある程度、学習をしたのでポートフォリオを制作することにしました。目的
私は、現在転職するために独学で、プログラミングを勉強しています。
また、転職活動において他人と差別化できるように作成中のポートフォリオの説明や工夫点などをアウトプットしていきます。開発環境
■言語(製作中の現時点)
PHP 7.4.2
mysql Ver 8.0.22
JavaScript (jQuery)
HTML
CSS■ローカル開発
MAMPポートフォリオ概要
・機能
ログイン機能、カレンダー、ToDoList
・細かな機能
ユーザー登録、ログイン、ログアウト
カレンダーの一般的な機能
日にち選択時にモーダルウィンドウ(予定入力欄)表示■今後実装予定
入力した予定の保存、更新、削除機能
予定一覧に「成功」、「失敗」欄を追加し、選択により画面が変わる
その日の予定の進捗を0時前に表示させ○%以上成功したら、○%以下しか成功できなかったらとそれぞれ表示詳細
■ターゲット
13歳以上をメインターゲット■ターゲット選定理由
13歳になると中学生になります。中学生になれば、自分で予定を組んだりしていく事が多いと思います。
12歳未満は主に保護者が予定を管理していると考えています。■開発した理由
現職で工程管理、マネジメント業務を行っており、毎日予定を立て、業務をしています。
その中で、その日に計画した全ての予定を終わらせ、毎日こなしていくことは大変だと感じています。
なので、少しでもモチベーションを上げれるように考え方を変え、ミッション風にアレンジしようと思い具現化する為に実装しました。■工夫点
一般的なスケジュール管理をゲーム感覚でできるように付加価値を与えた点。
13歳以上をメインターゲットにしているので、全てにおいてシンプルに制作しています。
引き続きポートフォリオ制作がんばります!
- 投稿日:2021-01-19T12:46:12+09:00
Laravel カラム追加 外部キーカラムの追加と削除
はじめに
・カラムを後から追加したい
・外部制約キーをつけたカラムを追加したい向けの内容となっています
マイグレーション 外部キーカラムを追加
1.migrationファイルの作成
例) offersテーブルにuser_idカラムを追加したい場合migration$ php artisan make:migration add_user_id_to_offers_table --table=offers命名規則は add_(カラム名)to(テーブル名)_table --table=(テーブル名)
でファイル名を作成します--table=(テーブル名)の部分では、カラムを追加のテーブルを指定して行っています。
migrationpublic function up() { Schema::table('offers', function (Blueprint $table) { $table->integer('user_id')->unsigned(); $table->foreign('user_id')->references('id')->on('users')->OnDelete('cascade'); }); }$ php artisan migrateさあ、これでマイグレーションするとカラム作成だ!
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint failsと思いきや,うまく外部制約キーのカラムがうまく作成できません。
調べてみると、原因は、「users.idに存在しないデータがoffers.user_idに存在しているから」だそうです。
上記のマイグレーションをしたときはoffersテーブルにはuser_idのカラムが入っていましたが、user_idが0でした。もともと外部キー制約は
「参照元フィールドに存在するデータのみ参照先フィールドのデータに存在できる」
という仕様です。そもそも,user_idが存在しない場合にoffers.user_idが存在しているのは外部キーの設定ができるはずもありません。
解決方法
今回の場合の対策として
1. 整合性の取れないデータはdeleteする
2. usersにデータを追加する
3. offersテーブルのuser_idをnullにする外部制約のキーでデータが一致しないとエラーになります。
そのときに
php artisan migrate:refresh
php artisan migrate:fresh
など一旦データを削除してまたmigrateをするとうまくいきますが、問題点として
すでにあるデータを削除してしまう
毎回、データを削除しないといけない
などがあります。本来なら最初にちゃんとテーブル設計したらと思いますが、開発の途中で機能追加するときもこういった場面があるのではないかなと思い記事を書いてみました。
データを挿入するなら
・seedやfactoryを使ってデモデータを挿入するなどのやり方があります。
今回僕がやった方法は, 外部制約キーにnullableを追加して解決しました。
migratepublic function up() { Schema::table('offers', function (Blueprint $table) { $table->integer('user_id')->unsigned()->nullable(); $table->foreign('user_id')->references('id')->on('users')->OnDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('offers', function (Blueprint $table) { $table->dropForeign('offers_user_id_foreign'); }); // Schema::table('offers', function (Blueprint $table) { // $table->dropColumn('user_id'); // }); }nullable()を追加すれば、migrateがうまくいきます。
down()のところには外部制約キーを削除できるように
$table->dropForeign('offers_user_id_foreign');
に設定します。
ただ、外部キーをNullにするのはどうかなぁと考えてしまいます。
ここら辺はSQLの知識が必要だなと思っています。外部キー制約カラムの追加を試している方はぜひ参考にしてみてください。
参考