20210119のMySQLに関する記事は8件です。

性能測定の準備をまとめてみました

はじめに

最近始まったプロジェクトの中で、開発している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あればお願いします!

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

外部キーを持つデータの削除でdependentオプションが効かず焦った

開発環境

Mac OS Catalina 10.15.7
ruby 2.6系
rails 6.0系

各モデルのアソシエーション

user.rb
has_many :posts
has_many :comments
post.rb
belongs_to :user
has_many :comments
comment.rb
belongs_to :user
belongs_to :post

エラー内容

スクリーンショット 2021-01-19 20.29.49.png

ActiveRecord::InvalidForeignKey in PostsController#destroy

Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails

このエラー自体は過去にも見たことがあったので、「あ〜はいはい、外部キーを持つデータの削除だから、dependentオプション付けなきゃダメなんでしょ」とdependentオプションを付けて再度削除を試みました。

user.rb
has_many :posts
has_many :comments, dependent: :destroy
post.rb
belongs_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は付けてなかったなと思い、変更してみたらうまくいきました。
まだまだ、理解しているようで理解できていないことが多いですね。

説明できる方いましたら、教えていただけると幸いです。

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

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

GoのフレームワークはGin、ORMにGORMを使用。

ディレクトリ構成

├── 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 --build

Goのコンテナに入ります。

$ docker-compose exec web bash

Goのコンテナの/appでマイグレーションを実行します。
実行されるSQLはapp/db/migrationsのファイルです。
usersとマイグレーション管理のためのschema_migrationsが作られます。

$ go run cmd/migrate/main.go -exec up

usersには名前(name)、年齢(age)、日時カラムを用意しました。

/db/migrations/1_create_users.up.sql
CREATE 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 DELETE

docker-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で接続するときはこうなります。

スクリーンショット 2021-01-03 11.24.13 (1).png
※パスワードは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.yml
version: "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:
      - web

MySQLコンテナ

MySQLのDockerfileはこれらの一般的な設定です。

タイムゾーンをAsia/Tokyoにする。
設定ファイルのmy.cnfをコピーする。
起動時に実行するinit.sqlをコピーする。

mysql/Dockerfile
FROM 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 3306

init.sqlではgo_sampleとテスト用のgo_sample_testを作ってユーザーの権限設定をします。

mysql/docker-entrypoint-initdb.d/init.sql
CREATE 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を事前にコピーしています。

Dockerfile
FROM golang:1.15

## 作業ディレクトリ
WORKDIR /app

# モジュール管理のファイルをコピー
COPY go/go.mod .
COPY go/go.sum .

# 外部パッケージのダウンロード
RUN go mod download

EXPOSE 9000

nginxコンテナ

nginx.confで読み込むdefault.confをコピーします。

nginx/Dockerfile
FROM nginx:1.19-alpine

COPY ./default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
nginx/default.conf
server {
    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://localhosthttp://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/87093e5941bfbc09bea8

cmd
アプリケーションのエントリーポイント。
サーバー起動とマイグレーション機能を配置。

db
マイグレーションで実行したいsqlファイルを配置。

pkg
アプリケーションの挙動に関わる部分。
モデル(model)、コントローラー(controller)、接続(connecter)を作成。

マイグレーション

マイグレーション周りはこちらを参考にさせていただきました。
https://qiita.com/tanden/items/7b4fb1686a61dd5f580d

golang-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 down

test用のデータベースに接続したいときはAPP_MODE=testで環境変数つきで実行します。

$ APP_MODE=test go run cmd/migrate/main.go -exec up

cmd/migrate/main.goのinit()でAPP_MODEがtestなら、データベースはDB_NAME_TESTを使うようにしてます。

cmd/migrate/main.go
func 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.go
package 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.go
package 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.go
package 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.go
package 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(&param); 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(&param); 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.go
package 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.go
package 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.go
package 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のような全部入りのフレームワークの偉大さも感じました。

長めの記事でしたが参考にしてもらえたらありがたいです!

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

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ファイル

Dockerfile
FROM 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.yml
version: '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

Gemfile
source '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.yml
default: &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で環境構築

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

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のことをしっかり理解していないですが、使いながら理解していきたい!!

とりあえず同じ様なエラーで前に進まない方の参考になればと思います。

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

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用のイメージをエミュレーションして動作させることになるっぽい。
エミュレーションするため遅くはなるかもしれない

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

PHP Portfolio 

はじめに

らむです。
今回は、初めてQiitaで投稿します。
理由としては、ある程度、学習をしたのでポートフォリオを制作することにしました。

目的

私は、現在転職するために独学で、プログラミングを勉強しています。
また、転職活動において他人と差別化できるように作成中のポートフォリオの説明や工夫点などをアウトプットしていきます。

開発環境

■言語(製作中の現時点)
PHP 7.4.2
mysql Ver 8.0.22
JavaScript (jQuery)
HTML
CSS

■ローカル開発
MAMP

ポートフォリオ概要

・機能
  ログイン機能、カレンダー、ToDoList
・細かな機能
  ユーザー登録、ログイン、ログアウト
  カレンダーの一般的な機能
  日にち選択時にモーダルウィンドウ(予定入力欄)表示

■今後実装予定
入力した予定の保存、更新、削除機能
予定一覧に「成功」、「失敗」欄を追加し、選択により画面が変わる
その日の予定の進捗を0時前に表示させ○%以上成功したら、○%以下しか成功できなかったらとそれぞれ表示

詳細

■ターゲット
13歳以上をメインターゲット

■ターゲット選定理由
13歳になると中学生になります。中学生になれば、自分で予定を組んだりしていく事が多いと思います。
12歳未満は主に保護者が予定を管理していると考えています。

■開発した理由
現職で工程管理、マネジメント業務を行っており、毎日予定を立て、業務をしています。
その中で、その日に計画した全ての予定を終わらせ、毎日こなしていくことは大変だと感じています。
なので、少しでもモチベーションを上げれるように考え方を変え、ミッション風にアレンジしようと思い具現化する為に実装しました。

■工夫点

一般的なスケジュール管理をゲーム感覚でできるように付加価値を与えた点。
13歳以上をメインターゲットにしているので、全てにおいてシンプルに制作しています。
 
引き続きポートフォリオ制作がんばります!

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

Laravel カラム追加 外部キーカラムの追加と削除

はじめに

・カラムを後から追加したい
・外部制約キーをつけたカラムを追加したい

向けの内容となっています

マイグレーション 外部キーカラムを追加

1.migrationファイルの作成
例) offersテーブルにuser_idカラムを追加したい場合

migration
$ php artisan make:migration add_user_id_to_offers_table --table=offers

命名規則は add_(カラム名)to(テーブル名)_table --table=(テーブル名)
でファイル名を作成します

--table=(テーブル名)の部分では、カラムを追加のテーブルを指定して行っています。

migration
public 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を追加して解決しました。

migrate
public 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の知識が必要だなと思っています。

外部キー制約カラムの追加を試している方はぜひ参考にしてみてください。

参考

Laravel7.x データベース: マイグレーション

Laravelで、外部キー制約の処理で SQLSTATE[23000] エラーが出て、マイグレートできません。

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