20201013のMySQLに関する記事は13件です。

MySql。OPTIMIZEで、DELETEでレコード削除した後のディスク領域を解放する方法

問題点。
MySqlのDELETEでレコードを大量に削除してもテーブルのディスク領域が解放されない。

解決法。
OPTIMIZE TABLEというテーブルを最適化するのを実行するとディスク領域がちゃんと解放される。

例。
テーブル名を"table_names"とする
SQL文は以下となる

OPTIMIZE TABLE table_names

これでディスク領域が解放される。

MySqlのバージョンが5.6.5以前だと注意が必要らしい。
自分はバージョン5.7なので気にしない。

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

yps並走備忘録 Task5 Webアプリの作成

今回の主な課題

・MySQLのVIEWテーブルの理解
・Bootstrap UIの導入
・Laravel Mixの使い方

事前準備

Node.jsとnpmのアップデート
sudo yum remove node npm -y
curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install nodejs -y

インストールで来たら下記コマンドでアップデートされていることを確認
$ node -v 例:v12.18.3
$ npm -v 例:6.14.6

一度Laravelのディレクトリに戻り、Laravel Mix(開発用)でビルドします
cd /var/www/html/yps
rm -rf ./node_modules
npm install && npm run dev

LaravelのuiにBootstrapを指定
composer require laravel/ui
php artisan ui bootstrap

Bootstrapをビルド
npm install && npm run dev

今回使用するデータをMySQLへ
1. VS Code内でターミナルを起動(ctrl+shift+@)
2. pwd ⇒Laravelのプロジェクトフォルダ(/var/www/html/yps)にいることを確認
3. mkdir resources/sql ⇒SQLファイル格納用のディレクトリを作成
4. mysqldump -u root -p -d worldcup2014db > resources/sql/worldcup2014db.sql ⇒テーブルの定義ファイルを取得(データは入手出来ていません)
5. grep -i 'create table' resources/sql/worldcup2014db.sql ⇒テーブルを確認[^1]
6. 各テーブル(countries, goals, pairings, players)にフィールド(expired_at, deleted_at, updated_at, created_at)を追加

5の結果

CREATE TABLE `countries` (
CREATE TABLE `goals` (
CREATE TABLE `goals_tmp` (
CREATE TABLE `pairings` (
CREATE TABLE `pairings_tmp` (
CREATE TABLE `players` (
CREATE TABLE `players_tmp` (

6の参考例

CREATE TABLE `countries` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `ranking` int(11) DEFAULT NULL,
  `group_name` varchar(1) DEFAULT NULL,
  `expired_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

MySQLでデータベースを削除⇒作り直し
1. mysql -u root -p
2. drop database worldcup2014db;
3. create database worldcup2014db;
4. use worldcup2014db;
5. source resources/sql/worldcup2014db.sql; ⇒ 作ったSQLファイルを元にテーブルを作成
6. show tables; ⇒ テーブルが作成されているか確認
7. OKならexit;でMySQL CLiから出る

Task 3 同様にデータを取得~流し込みの準備
1. cd /tmp
2. sudo yum install wget unzip -y
3. wget http://tech.pjin.jp/wp-content/uploads/2016/04/worldcup2014.zip
4. unzip http://worldcup2014.zip
5. ls -la worldcup2014.sql
6. 各テーブル(countries, goals, goals_tmp, pairings, pairings_tmp, players, players_tmp)のCREATE TABLE ~ DEFAULT CHAESET=utf8;までを削除(↓の部分)

CREATE TABLE `countries` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `ranking` int(11) DEFAULT NULL,
  `group_name` varchar(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

**データの流し込み
7. mysql -u root -p -D worldcup2014db
8. source /tmp/worldcup2014.sql; //warningが出ますが特に問題はないそうです
9. 各テーブルのデータ件数をチェックして下記になっていればOK

mysql> select count(*) from countries;
+----------+
| count(*) |
+----------+
|       32 |
+----------+

mysql> select count(*) from goals;
+----------+
| count(*) |
+----------+
|      188 |
+----------+

mysql> select count(*) from pairings;
+----------+
| count(*) |
+----------+
|      144 |
+----------+

mysql> select count(*) from players;
+----------+
| count(*) |
+----------+
|      736 |
+----------+

問題なければexit;でMySQL Cliから出る

簡易アプリケーションの作成

使うデータは用意できたので、今度はデータを使ってアプリケーションを作成してみます。

モデルクラスの作成
Task3で作成したモデルクラスを削除
cd /var/www/html/yps
rm app/Models/Player.php

以下のコマンドを打ってデータベースと連動したモデルクラスを作成します
php artisan make:model Models/Country -m
php artisan make:model Models/Goal -m
php artisan make:model Models/Pairing -m
php artisan make:model Models/Player -m

上記で作成した各モデルクラスに明示的にテーブルを指定し、ついでに$dateも指定します。
※以下はPlayerモデルの例です

Player.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Player extends Model
{
    protected $table = "players";
    protected $dates = ["expired_at", "deleted_at", "updated_at", "created_at"];
}

コントローラークラスの作成
php artisan make:controller CountryController --resource --model=Models/Country
php artisan make:controller GoalController --resource --model=Models/Goal
php artisan make:controller PairingController --resource --model=Models/Pairing
php artisan make:controller PlayerController --resource --model= Models/Player

php artisan make:controller WelcomeController --resource

Viewの作成
resources/views/welcome.blade.phpに以下をコピペ

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <title>yotaro prg</title>

        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">

        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
        <meta name="generator" content="Jekyll v4.1.1">

        <!-- Bootstrap core CSS -->
        <link href="{{ asset('css/app.css') }}" rel="stylesheet">
        <!-- Favicons -->
        <link rel="apple-touch-icon" href="/yotaro.jpg" sizes="180x180">
        <link rel="icon" href="/yotaro.jpg" sizes="32x32" type="image/jpg">
        <link rel="icon" href="/yotaro.jpg" sizes="16x16" type="image/jpg">
        <?php /*
        <link rel="manifest" href="/docs/4.5/assets/img/favicons/manifest.json">
        <link rel="mask-icon" href="/docs/4.5/assets/img/favicons/safari-pinned-tab.svg" color="#563d7c">
        */ ?>
        <link rel="icon" href="/yotaro.jpg">
        <meta name="theme-color" content="#563d7c">
        <style>
            body {
                padding-top: 5rem;
            }
            .starter-template {
                padding: 3rem 1.5rem;
                text-align: center;
            }
            .bd-placeholder-img {
                font-size: 1.125rem;
                text-anchor: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }
            @media (min-width: 768px) {
                .bd-placeholder-img-lg {
                    font-size: 3.5rem;
                }
            }
        </style>
    </head>
    <body>
        <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
            <a class="navbar-brand" href="#">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarsExampleDefault">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Home 
                            <span class="sr-only">(current)</span>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                        <div class="dropdown-menu" aria-labelledby="dropdown01">
                            <a class="dropdown-item" href="#">Action</a>
                            <a class="dropdown-item" href="#">Another action</a>
                            <a class="dropdown-item" href="#">Something else here</a>
                        </div>
                    </li>
                </ul>
                <form class="form-inline my-2 my-lg-0">
                    <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
                    <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button>
                </form>
            </div>
        </nav>
        <main role="main" class="container">
                <h1>WorldCup 2014 選手一覧</h1>
                </br>
                <table class="table table-striped table-hover table-sm table-responsive-sm">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">country</th>
                            <th scope="col">uniform_num</th>
                            <th scope="col">position</th>
                            <th scope="col">name</th>
                            <th scope="col">club</th>
                            <th scope="col">birth</th>
                            <th scope="col">height</th>
                            <th scope="col">weight</th>
                            <th scope="col">total_goals</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($players as $player)
                        <tr>
                            <td>{{ $player->id }}</td>
                            <td>{{ $player->c_name }}</td>
                            <td>{{ $player->uniform_num }}</td>
                            <td>{{ $player->position }}</td>
                            <td>{{ $player->name }}</td>
                            <td>{{ $player->club }}</td>
                            <td>{{ $player->birth }}</td>
                            <td>{{ $player->height }}</td>
                            <td>{{ $player->weight }}</td>
                            <td>{{ $player->t_goals }}</td>
                        </tr>
                        @endforeach
                    </tbody>
                </table>
                <div class="row justify-content-end">
                    {{ $players->links() }}
                </div>
        </main>
        <!-- /.container -->
        <script src="{{ asset('js/app.js') }}"></script>
    </body>
</html>

ルーター作成
routes/web.phpに下記を記述

web.php
Route::get('/', 'WelcomeController@index');
Route::resource('players', 'PlayerController');
Route::resource('countries', 'CountryController');
Route::resource('goals', 'GoalController');
Route::resource('pairings', 'PairingController');

課題はここから…

  1. テーブルビューを1つ(あるいは2つ)追加
  2. functionを1つ追加
  3. controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします

答えはコチラ

Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストール

イージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5

以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)

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

yps並走備忘録 Task5 簡易Webアプリの作成(SQLとモデルを理解する編)

今回の主な課題

・MySQLのVIEWテーブルの理解
・Bootstrap UIの導入
・Laravel Mixの使い方

事前準備

Node.jsとnpmのアップデート
sudo yum remove node npm -y
curl -sL https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install nodejs -y

インストールで来たら下記コマンドでアップデートされていることを確認
$ node -v 例:v12.18.3
$ npm -v 例:6.14.6

一度Laravelのディレクトリに戻り、Laravel Mix(開発用)でビルドします
cd /var/www/html/yps
rm -rf ./node_modules
npm install && npm run dev

LaravelのuiにBootstrapを指定
composer require laravel/ui
php artisan ui bootstrap

Bootstrapをビルド
npm install && npm run dev

今回使用するデータをMySQLへ
1. VS Code内でターミナルを起動(ctrl+shift+@)
2. pwd ⇒Laravelのプロジェクトフォルダ(/var/www/html/yps)にいることを確認
3. mkdir resources/sql ⇒SQLファイル格納用のディレクトリを作成
4. mysqldump -u root -p -d worldcup2014db > resources/sql/worldcup2014db.sql ⇒テーブルの定義ファイルを取得(データは入手出来ていません)
5. grep -i 'create table' resources/sql/worldcup2014db.sql ⇒テーブルを確認[^1]
6. 各テーブル(countries, goals, pairings, players)にフィールド(expired_at, deleted_at, updated_at, created_at)を追加

5の結果

CREATE TABLE `countries` (
CREATE TABLE `goals` (
CREATE TABLE `goals_tmp` (
CREATE TABLE `pairings` (
CREATE TABLE `pairings_tmp` (
CREATE TABLE `players` (
CREATE TABLE `players_tmp` (

6の参考例

CREATE TABLE `countries` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  `ranking` int(11) DEFAULT NULL,
  `group_name` varchar(1) DEFAULT NULL,
  `expired_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `deleted_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

MySQLでデータベースを削除⇒作り直し
1. mysql -u root -p
2. drop database worldcup2014db;
3. create database worldcup2014db;
4. use worldcup2014db;
5. source resources/sql/worldcup2014db.sql; ⇒ 作ったSQLファイルを元にテーブルを作成
6. show tables; ⇒ テーブルが作成されているか確認
7. OKならexit;でMySQL CLiから出る

Task 3 同様にデータを取得~流し込みの準備
1. cd /tmp
2. sudo yum install wget unzip -y
3. wget http://tech.pjin.jp/wp-content/uploads/2016/04/worldcup2014.zip
4. unzip http://worldcup2014.zip
5. ls -la worldcup2014.sql
6. 各テーブル(countries, goals, goals_tmp, pairings, pairings_tmp, players, players_tmp)のCREATE TABLE ~ DEFAULT CHAESET=utf8;までを削除(↓の部分)

CREATE TABLE `countries` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `ranking` int(11) DEFAULT NULL,
  `group_name` varchar(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

**データの流し込み
7. mysql -u root -p -D worldcup2014db
8. source /tmp/worldcup2014.sql; //warningが出ますが特に問題はないそうです
9. 各テーブルのデータ件数をチェックして下記になっていればOK

mysql> select count(*) from countries;
+----------+
| count(*) |
+----------+
|       32 |
+----------+

mysql> select count(*) from goals;
+----------+
| count(*) |
+----------+
|      188 |
+----------+

mysql> select count(*) from pairings;
+----------+
| count(*) |
+----------+
|      144 |
+----------+

mysql> select count(*) from players;
+----------+
| count(*) |
+----------+
|      736 |
+----------+

問題なければexit;でMySQL Cliから出る

簡易アプリケーションの作成

使うデータは用意できたので、今度はデータを使ってアプリケーションを作成してみます。

モデルクラスの作成
Task3で作成したモデルクラスを削除
cd /var/www/html/yps
rm app/Models/Player.php

以下のコマンドを打ってデータベースと連動したモデルクラスを作成します
php artisan make:model Models/Country -m
php artisan make:model Models/Goal -m
php artisan make:model Models/Pairing -m
php artisan make:model Models/Player -m

上記で作成した各モデルクラスに明示的にテーブルを指定し、ついでに$dateも指定します。
※以下はPlayerモデルの例です

Player.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Player extends Model
{
    protected $table = "players";
    protected $dates = ["expired_at", "deleted_at", "updated_at", "created_at"];
}

コントローラークラスの作成
php artisan make:controller CountryController --resource --model=Models/Country
php artisan make:controller GoalController --resource --model=Models/Goal
php artisan make:controller PairingController --resource --model=Models/Pairing
php artisan make:controller PlayerController --resource --model= Models/Player

php artisan make:controller WelcomeController --resource

Viewの作成
resources/views/welcome.blade.phpに以下をコピペ

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <title>yotaro prg</title>

        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">

        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
        <meta name="generator" content="Jekyll v4.1.1">

        <!-- Bootstrap core CSS -->
        <link href="{{ asset('css/app.css') }}" rel="stylesheet">
        <!-- Favicons -->
        <link rel="apple-touch-icon" href="/yotaro.jpg" sizes="180x180">
        <link rel="icon" href="/yotaro.jpg" sizes="32x32" type="image/jpg">
        <link rel="icon" href="/yotaro.jpg" sizes="16x16" type="image/jpg">
        <?php /*
        <link rel="manifest" href="/docs/4.5/assets/img/favicons/manifest.json">
        <link rel="mask-icon" href="/docs/4.5/assets/img/favicons/safari-pinned-tab.svg" color="#563d7c">
        */ ?>
        <link rel="icon" href="/yotaro.jpg">
        <meta name="theme-color" content="#563d7c">
        <style>
            body {
                padding-top: 5rem;
            }
            .starter-template {
                padding: 3rem 1.5rem;
                text-align: center;
            }
            .bd-placeholder-img {
                font-size: 1.125rem;
                text-anchor: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
            }
            @media (min-width: 768px) {
                .bd-placeholder-img-lg {
                    font-size: 3.5rem;
                }
            }
        </style>
    </head>
    <body>
        <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
            <a class="navbar-brand" href="#">Navbar</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarsExampleDefault">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Home 
                            <span class="sr-only">(current)</span>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                        <div class="dropdown-menu" aria-labelledby="dropdown01">
                            <a class="dropdown-item" href="#">Action</a>
                            <a class="dropdown-item" href="#">Another action</a>
                            <a class="dropdown-item" href="#">Something else here</a>
                        </div>
                    </li>
                </ul>
                <form class="form-inline my-2 my-lg-0">
                    <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
                    <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button>
                </form>
            </div>
        </nav>
        <main role="main" class="container">
                <h1>WorldCup 2014 選手一覧</h1>
                </br>
                <table class="table table-striped table-hover table-sm table-responsive-sm">
                    <thead>
                        <tr>
                            <th scope="col">#</th>
                            <th scope="col">country</th>
                            <th scope="col">uniform_num</th>
                            <th scope="col">position</th>
                            <th scope="col">name</th>
                            <th scope="col">club</th>
                            <th scope="col">birth</th>
                            <th scope="col">height</th>
                            <th scope="col">weight</th>
                            <th scope="col">total_goals</th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach ($players as $player)
                        <tr>
                            <td>{{ $player->id }}</td>
                            <td>{{ $player->c_name }}</td>
                            <td>{{ $player->uniform_num }}</td>
                            <td>{{ $player->position }}</td>
                            <td>{{ $player->name }}</td>
                            <td>{{ $player->club }}</td>
                            <td>{{ $player->birth }}</td>
                            <td>{{ $player->height }}</td>
                            <td>{{ $player->weight }}</td>
                            <td>{{ $player->t_goals }}</td>
                        </tr>
                        @endforeach
                    </tbody>
                </table>
                <div class="row justify-content-end">
                    {{ $players->links() }}
                </div>
        </main>
        <!-- /.container -->
        <script src="{{ asset('js/app.js') }}"></script>
    </body>
</html>

ルーター作成
routes/web.phpに下記を記述

web.php
Route::get('/', 'WelcomeController@index');
Route::resource('players', 'PlayerController');
Route::resource('countries', 'CountryController');
Route::resource('goals', 'GoalController');
Route::resource('pairings', 'PairingController');

課題はここから…

  1. テーブルビューを1つ(あるいは2つ)追加
  2. functionを1つ追加
  3. controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします

答えはコチラ

Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストール

イージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5

以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)

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

EC2上のmysqlにSequelProから接続してデータを挿入する

SequelProを使ってデータベースにデータを挿入する方法を備忘録として残します。

まずSequelProを開きましょう!するとこんな画面ですよね
84d6295b0f55a2bbac8fec55f2ef5166.png

左下の+ボタンを押す

お気に入りのところに新しく追加されます
あああ.png
名前はわかりやすいように自分で変えましょう

標準にカーソルがあっているものをSSHに合わせる

いいいい.png
ここのフォームに必要な情報を入力していくとデータベースにアクセスできます

・MySQLホスト: 127.0.0.1 ←デフォルト

・ユーザー名: root

・パスワード: EC2上のMySQLのrootユーザのパスワード

・データベース: 空欄

・ポート: 3306

・SSHホスト: EC2のElastic IP

・SSHユーザー: ec2-user

・SSHパスワード:  左の鍵のアイコンをクリックしましょう。finderに繋がります。EC2のキーペアをダウンロードしたときのファイルを選択する。(Finderにあるが隠れてい普段は見ることができない)

・ポート:  空欄で良い

・SSLを使用して接続:  チェックなし

SSH鍵を表示するには、まずキーが.sshディレクトリにあるのを確認.sshディレクトリでlsコマンドで確認

hoge@hogenoMacBook-Air .ssh % ls
キーペア名.pem known_hosts

.sshディレクトリに〜.pemがあるのを確認したらfinderでこのファイルを探す。ただ、sshディレクトリは隠しディレクトリなのでそれを表示するためにはfinder上で[コマンド]+[シフト]+[ドット]を打つと隠しディレクトリが現れる。

スクリーンショット 2020-10-13 22.16.53.png

このなかにpemキーのファイルがあるので選択

この手順で本番環境のデータベース にアクセスできました。

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

docker を用いてlaravel6+nuxt.js+mysql+nginx構築

laravel6+nuxt.js+mysqlの構築

laravel,mysql,nginxの構築は@simotarooさんの記事が優秀なのでこちらを参考にしてください
https://qiita.com/shimotaroo/items/29f7878b01ee4b99b951

本題に関係ないんですけどngixってエンジンエックスって読むんですね,,,(笑

nuxt.jsの構築

@simotarooさんの記事で作成したdocker-compose.ymlに PORT:3000, HOST: 0.0.0.0を追加
理由としてコンテナで設定されているポート番号とnode.jsのポート番号が違うままだと繋がらないためです

docker-compose.yml
#docker-compose.ymlのバージョン
version: '3.8'
#docker volumeの設定
volumes:
  docker-volume:

#services以下に各コンテナの設定を書く
services:
  #Webサーバーのコンテナ
  web:
    image: nginx:1.18
    ports:
      - '8000:80'
    depends_on:
      - app
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/html
  #アプリケーションのコンテナ
  app:
    build: ./docker/php
    volumes:
      - .:/var/www/html
    environment:
      PORT: 3000 #追加
      HOST: 0.0.0.0 #追加
    ports:
      - 3000:3000
  #データベースのコンテナ
  db:
    image: mysql:5.7
    ports:
      - '3306:3306'
    environment:
      MYSQL_DATABASE: ######
      MYSQL_USER: #####
      MYSQL_PASSWORD: #######
      MYSQL_ROOT_PASSWORD: ##########
      TZ: 'Asia/Tokyo'
    volumes:
      - docker-volume:/var/lib/mysql

dockerfileに RUN npm install -g create-nuxt-app を追加

dockerfile
FROM php:7.2-fpm

#composerのインストール
COPY --from=composer:1.10 /usr/bin/composer /usr/bin/composer

#npmのインストール
COPY --from=node:10.22 /usr/local/bin /usr/local/bin
COPY --from=node:10.22 /usr/local/lib /usr/local/lib

#パッケージ管理ツールapt-getの更新と必要パッケージのインストール
RUN apt-get update \
&& apt-get install -y \
git \
zip \
unzip \
&& docker-php-ext-install pdo_mysql bcmath

RUN npm install -g create-nuxt-app #追加
#コンテナ内に入った時のディレクトリを指定
WORKDIR /var/www/html

追加後はdocker-compose buildし変更を反映

反映できたらdocker内で

tarminal
root@########:/var/www/html npx create-nuxt-app ディレクトリ名

で実行

tarminal
?  Successfully created project ディレクトリ名

  To get started:

    cd ディレクトリ名
    npm run dev

  To build & start for production:

    cd ディレクトリ名
    npm run build
    npm run start

成功したらこんな画面になります。
作成したディレクトリ内に入り,npm run devを実行後動作確認で以下のURLに入ってnode.jsの画面が出れば成功です
http://localhost:3000/
お疲れ様でした?

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

Java・ツイッタークローン・タスク管理システム⑥削除機能をつける

はじめに

Javaを使って初めてアプリケーションを作成する人にむけて記事を書いてみようと思います。
ポートフォリオや、会社の研修課題作成の参考にしていただければ幸いです。
今回は、タスクマネージャーを作成します。
これを応用することで、ツイッタークローンの作成にも活かすことができます。

アプリケーションの機能ごとに記事を投稿していきます。
1.データベース作成
2.ログイン機能
3.タスクの登録機能
4.一覧表示
  -ソート機能
  -検索機能
5.編集機能
6.削除機能
7.排他制御について

*詳しい説明はコード内に書いてありますので、コピペする人は消して使ってください

実行環境

eclipse4.16.0
Tomcat9
Java11
Mysql5.7

目次
1.viewの作成
2.DAOの作成
3.サーブレット作成
4.次回予告

viewの作成

タスクの削除前にここでタスクの詳細の確認をしてもらう

task-delete.java
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>タスク削除</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
</head>
<body>
<jsp:useBean id="task" scope="request" class="model.entity.TaskBean"/>
<%
String category_name = (String)request.getAttribute("category_name");
String status_name = (String)request.getAttribute("status_name");
String user_name = (String)request.getAttribute("user_name");
int version = (int)request.getAttribute("version");
%>
    <jsp:include page="header.jsp"/>
    <div class="contain mt-4 mr-5 ml-5">
        <div class="row justify-content-center">
            <h3>タスク削除</h3>
        </div>
        <table class="table">
          <thead>
            <tr>
              <th scope="col"></th>
              <th scope="col">このタスクを削除しますか?</th>
            </tr>
          </thead>
          <tbody>
          <tr>
              <th scope="row">タスクID</th>
              <td><jsp:getProperty property="task_id" name="task"/></td>
            </tr>
            <tr>
              <th scope="row">タスク名</th>
              <td><jsp:getProperty property="task_name" name="task"/></td>
            </tr>
            <tr>
              <th scope="row">カテゴリー</th>
              <td><jsp:getProperty property="category_id" name="task"/>:<%=category_name %></td>
            </tr>
            <tr>
              <th scope="row">期限</th>
              <td><jsp:getProperty property="limit_date" name="task"/></td>
            </tr>
            <tr>
              <th scope="row">ユーザ名</th>
              <td><%=user_name %></td>
            </tr>
            <tr>
              <th scope="row">ステータス</th>
              <td><jsp:getProperty property="status_code" name="task"/>:<%=status_name %></td>
            </tr>
            <tr>
              <th scope="row">メモ</th>
              <td><jsp:getProperty property="memo" name="task"/></td>
            </tr>
            <tr>
              <th scope="row">登録日時</th>
              <td><jsp:getProperty property="create_datetime" name="task"/></td>
            </tr>
            <tr>
              <th scope="row">更新日時</th>
              <td><jsp:getProperty property="update_datetime" name="task"/></td>
            </tr>
          </tbody>
        </table>
        <form action="task-delete-servlet" method="post">
            <input type="hidden" name="task_id" value="<jsp:getProperty property="task_id" name="task"/>">
            <input type="hidden" name="version" value="<%=version %>">
            <button type="submit" class="btn btn-outline-danger">削除</button>
        </form>
    </div>

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"></script>
</body>
</html>

タスクが無事削除できたらこの画面にて確認してもらう。

task-delete-comp.java
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>削除完了</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/task-edit-failure.css">
<body>
    <jsp:include page="header.jsp"/>
    <div class="contain">
        <div class="box">
             <h3>タスクの削除が完了しました</h3>
        </div>
    </div>
</body>
</html>

タスクの削除に失敗したらこの画面を表示
(次回行う排他制御などなんらかの理由で削除できなかった時にはこちらでお知らせする)

task-delete-failure.java
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>タスクの削除失敗</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/task-edit-failure.css">
</head>
<body>
<jsp:include page="header.jsp"/>
    <div class="contain">
        <div class="box">
          <h3>タスクの削除に失敗しました</h3>
        </div>
    </div>
</body>
</html>

DAOの作成

こちらでは前回作成したタスク編集記事に出てくるメソッドも出てきます。
必要な方だけみてください

model.dao.TaskDAO.java
/**
     * task_idを引数に特定のタスクを取得する
     * @param task_id
     * @return
     * @throws SQLException
     * @throws ClassNotFoundException
     */
    public TaskBean getSpecificTask(int task_id) throws SQLException, ClassNotFoundException {
         TaskBean task = new TaskBean();
         String sql = "select * from t_task where task_id = ?";
         try(Connection con = ConnectionManager.getConnection();
                 PreparedStatement pstmt = con.prepareStatement(sql);){
             pstmt.setInt(1, task_id);
             ResultSet res = pstmt.executeQuery();
             //taskオブジェクトへカラム情報を格納していく
             while(res.next()) {
                task.setTask_id(task_id);
                task.setTask_name(res.getString("task_name"));
                task.setCategory_id(res.getInt("category_id"));
                task.setLimit_date(res.getDate("limit_date"));
                task.setUser_id(res.getString("user_id"));
                task.setStatus_code(res.getString("status_code"));
                task.setMemo(res.getString("memo"));
                task.setCreate_datetime(res.getTimestamp("create_datetime"));
                task.setUpdate_datetime(res.getTimestamp("update_datetime"));
                task.setVersion(res.getInt("version"));
             }
         }
         return task;
     }

/**
      * タスク削除用メソッド
      * @param task_id
      * @return sum
      * @throws SQLException
      * @throws ClassNotFoundException
      */
     public int deleteTask(int task_id) throws SQLException, ClassNotFoundException {
         String sql = "delete from t_task where task_id = ?";
         int sum = 0;
             try(Connection con = ConnectionManager.getConnection();
             PreparedStatement pstmt = con.prepareStatement(sql)){
                 pstmt.setInt(1, task_id);
                 sum = pstmt.executeUpdate();
         }
         return sum;
     }

サーブレット作成

こちらで、タスク一覧画面から選択したタスクの情報を取得してセッションスコープへ入れて、確認画面へ転送する

servlet.TaskDeleteDetailServlet.java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub

        TaskDAO taskdao = new TaskDAO();
        TaskBean task = new TaskBean();
        CategoryDAO categorydao = new CategoryDAO();
        StatusDAO statusdao = new StatusDAO();
        UserDAO userdao = new UserDAO();

        //リクエストパラメータ取得
        request.setCharacterEncoding("UTF-8");
        int task_id = Integer.parseInt(request.getParameter("task_id"));

        try {
            task = taskdao.getSpecificTask(task_id);
            int category_id = task.getCategory_id();
            String status_code = task.getStatus_code();
            String user_id = task.getUser_id();
            String category_name = categorydao.getCategoryName(category_id);
            String status_name = statusdao.getStatusName(status_code);
            String user_name = userdao.getUserName(user_id);
            int version = task.getVersion();
            request.setAttribute("task", task);
            request.setAttribute("category_name", category_name);
            request.setAttribute("status_name", status_name);
            request.setAttribute("user_name", user_name);
            request.setAttribute("version", version);

        }catch(SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        RequestDispatcher rd = request.getRequestDispatcher("task-delete.jsp");
        rd.forward(request, response);
    }
servlet.TaskDeleteServlet.java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //リクエストパラメータ取得
        request.setCharacterEncoding("UTF-8");
        int task_id = Integer.parseInt(request.getParameter("task_id"));
        int version =  Integer.parseInt(request.getParameter("version"));
        TaskDAO taskdao = new TaskDAO();

        List<String> error = new ArrayList<String>();


        try{
            //versionの確認
            int current_version = taskdao.getVersion(task_id);
            if(current_version != version) {
                error.add("このタスクは他の人によって上書きがされています");
                request.setAttribute("error", error);
                RequestDispatcher rd = request.getRequestDispatcher("task-update-failure.jsp");
                rd.forward(request, response);
            }

            //タスクの削除実行
            taskdao.deleteTask(task_id);
            RequestDispatcher rd = request.getRequestDispatcher("task-delete-comp.jsp");
            rd.forward(request, response);
        } catch(SQLException | ClassNotFoundException e) {
            RequestDispatcher rd = request.getRequestDispatcher("task-delete-failure.jsp");
            rd.forward(request, response);
        }
    }

次回予告

今回は削除機能の実装をしてきました。
初めてのアプリケーションに最低限の機能はここまでの記事にあるコードを使えばできます。
次回は、排他制御と言う複数のユーザが同時にアプリケーションを使うことを考えた時の処理について紹介してまいります。

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

Java・ツイッタークローン・タスク管理システム⑤編集機能をつける

はじめに

Javaを使って初めてアプリケーションを作成する人にむけて記事を書いてみようと思います。
ポートフォリオや、会社の研修課題作成の参考にしていただければ幸いです。
今回は、タスクマネージャーを作成します。
これを応用することで、ツイッタークローンの作成にも活かすことができます。

アプリケーションの機能ごとに記事を投稿していきます。
1.データベース作成
2.ログイン機能
3.タスクの登録機能
4.一覧表示
  -ソート機能
  -検索機能
5.編集機能
6.削除機能
7.排他制御について

*詳しい説明はコード内に書いてありますので、コピペする人は消して使ってください

実行環境

eclipse4.16.0
Tomcat9
Java11
Mysql5.7

目次
1.viewの作成
2.DAOの作成
3.サーブレット作成
4.次回予告

viewの作成

今回は、編集ページを作っていきます。以前記事にしたタスク登録画面に大変似ているので、簡単に確認してください。
タスク編集は、ログインユーザなら自分が投稿した物以外も編集することができるようになっております。

task-update.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>タスク編集</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/task-insert.css">
</head>
<body>
<jsp:include page="header.jsp"/>
<jsp:useBean id="task" scope="session" class="model.entity.TaskBean"/>

    <div class="contain">
        <h3>タスク登録</h3>
        <form action="task-update-servlet" method="post">
            <table class="form-table" border="1">
                <tbody>
                    <tr>
                        <th>タスク名</th>
                            <td>
                            <div class="input-key">
                                <input type="text" class="form-control" name="task_name" value="<jsp:getProperty property="task_name" name="task"/>">
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <th>カテゴリー</th>
                        <td>
                            <div class="input-key">
                                <select class="form-control" name="category_id">
                                    <option value="<jsp:getProperty property="category_id" name="task"/>">変更なし</option>
                                    <option value="1">新商品A:開発プロジェクト</option>
                                    <option value="2">既存商品B:改良プロジェクト</option>
                                </select>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <th>期限</th>
                            <td>
                            <div class="input-key">
                                <input type="date" name="limit_date" class="form-control" value="<jsp:getProperty property="limit_date" name="task"/>">
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <th>ステータス</th>
                        <td>
                            <div class="input-key">
                                <select class="form-control" name="status_code">
                                    <option value="<jsp:getProperty property="status_code" name="task"/>">変更なし</option>
                                    <option value="00">未着手</option>
                                    <option value="50">着手</option>
                                    <option value="99">完了</option>
                                </select>
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <th>メモ</th>
                            <td>
                            <div class="input-key">
                                <input type="text" class="form-control" name="memo" value="<jsp:getProperty property="memo" name="task"/>">
                            </div>
                        </td>
                    </tr>
                    <tr>
                        <th>作成日</th>
                        <td><jsp:getProperty property="create_datetime" name="task"/><input type="hidden" name="create_datetime" value="<jsp:getProperty property="create_datetime" name="task"/>"></td>
                    </tr>
                    <tr>
                        <th>
                            <input type="hidden" name="task_id" value="<jsp:getProperty property="task_id" name="task"/>">
                            <input type="hidden" name="version" value='<jsp:getProperty property="version" name="task"/>'>
                            <input type="submit" value="更新する" class="input-submit"></th>
                        <td></td>
                    </tr>
                </tbody>
            </table>
        </form>
    </div>
</body>
</html>

さらに編集に成功した時の画面も用意します。
こちらには、編集した内容が表示されるようになっている。

task-update-comp.java
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="model.entity.TaskBean"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>更新完了</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/task-edit-failure.css">
</head>
<body>
<jsp:useBean id="task" class="model.entity.TaskBean" scope="session"/>
<%
String category_name = (String)request.getAttribute("category_name");
String status_name = (String)request.getAttribute("status_name");
%>
    <jsp:include page="header.jsp"/>
    <div class="contain">
        <div class="box">
            <h3>タスクの更新が完了しました</h3>
            <p>以下の内容で更新しました</p>
            <hr>
            <p>
                タスク名:<jsp:getProperty property="task_name" name="task"/><br>
                カテゴリー:<jsp:getProperty property="category_id" name="task"/>:<%=category_name %><br>
                期限:<jsp:getProperty property="limit_date" name="task"/><br>
                ステータス:<jsp:getProperty property="status_code" name="task"/>:<%=status_name %><br>
                メモ:<jsp:getProperty property="memo" name="task"/><br>
             </p>
         </div>
    </div>
</body>
</html>

さらに失敗した時の画面も用意する
失敗理由も表示するようにする

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="java.util.List"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>タスク更新失敗</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/task-edit-failure.css">
</head>
<body>
<%
List<String> error = (List<String>)request.getAttribute("error");
%>
    <jsp:include page="header.jsp"/>
    <div class="contain">
        <div class="box">
            <h3>タスクの更新に失敗しました</h3>
            <hr>
            <p>*下記の原因が考えられます</p>
            <ul>
<%for(String er : error){ %>
                <li><%=er %></li>
<%} %>
            </ul>
        </div>
    </div>
</body>
</html>

DAOの作成

タスクの更新用メソッドと指定したタスクを取得するメソッドの作成

model.dao.TaskDAO.java
 /**
      * タスク更新用メソッド
      * @param task
      * @return sum
      * @throws SQLException
      * @throws ClassNotFoundException
      */
     public int updateTask(TaskBean task) throws SQLException, ClassNotFoundException {
         String sql = "update t_task set task_name = ?, category_id = ?, limit_date = ?, status_code = ?, memo = ?, version = version + 1 where task_id = ? and version = ?";
         int sum = 0;
         try(Connection con = ConnectionManager.getConnection();
                 PreparedStatement pstmt = con.prepareStatement(sql)){
             pstmt.setString(1, task.getTask_name());
             pstmt.setInt(2, task.getCategory_id());
             pstmt.setDate(3, task.getLimit_date());
             pstmt.setString(4, task.getStatus_code());
             pstmt.setString(5, task.getMemo());
             pstmt.setInt(6, task.getTask_id());
             pstmt.setInt(7, task.getVersion());
             sum = pstmt.executeUpdate();
         }
         return sum;
     }

/**
     * task_idを引数に特定のタスクを取得する
     * @param task_id
     * @return
     * @throws SQLException
     * @throws ClassNotFoundException
     */
    public TaskBean getSpecificTask(int task_id) throws SQLException, ClassNotFoundException {
         TaskBean task = new TaskBean();
         String sql = "select * from t_task where task_id = ?";
         try(Connection con = ConnectionManager.getConnection();
                 PreparedStatement pstmt = con.prepareStatement(sql);){
             pstmt.setInt(1, task_id);
             ResultSet res = pstmt.executeQuery();
             //taskオブジェクトへカラム情報を格納していく
             while(res.next()) {
                task.setTask_id(task_id);
                task.setTask_name(res.getString("task_name"));
                task.setCategory_id(res.getInt("category_id"));
                task.setLimit_date(res.getDate("limit_date"));
                task.setUser_id(res.getString("user_id"));
                task.setStatus_code(res.getString("status_code"));
                task.setMemo(res.getString("memo"));
                task.setCreate_datetime(res.getTimestamp("create_datetime"));
                task.setUpdate_datetime(res.getTimestamp("update_datetime"));
                task.setVersion(res.getInt("version"));
             }
         }
         return task;
     }

サーブレットの作成

一覧画面から選択したタスクの情報をセッションスコープへ保存して転送先(task-update.java)で表示・編集する

servlet.TaskUpdateDetailServlet.java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        //リクエストパラメータの取得
        request.setCharacterEncoding("UTF-8");
        int task_id = Integer.parseInt(request.getParameter("task_id"));

        TaskDAO dao = new TaskDAO();
        TaskBean task = new TaskBean();

        try {
            //指定したタスクをsessionスコープへ保存
            task = dao.getSpecificTask(task_id);
            HttpSession session = request.getSession();
            session.setAttribute("task", task);
            RequestDispatcher rd = request.getRequestDispatcher("task-update.jsp");
            rd.forward(request, response);
        }catch(SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        }

編集後に下記のサーブレットにてDBの処理と画面遷移を行う

servlet.TaskUpdateServket.java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub

        TaskDAO taskdao = new TaskDAO();
        CategoryDAO categorydao = new CategoryDAO();
        StatusDAO statusdao = new StatusDAO();

        //リクエストパラメータ取得
        request.setCharacterEncoding("UTF-8");
        int task_id = Integer.parseInt(request.getParameter("task_id"));
        String task_name = request.getParameter("task_name");
        String category_id_str = request.getParameter("category_id");
        String limit_date_check = request.getParameter("limit_date");
        String status_code = request.getParameter("status_code");
        String memo = request.getParameter("memo");
        int version = Integer.parseInt(request.getParameter("version"));

        //error表示を保存するリスト生成
        List<String> error = new ArrayList<String>();

        //sessionチェック
        HttpSession session = request.getSession();
        boolean sessioncheck = (boolean)session.getAttribute("login");
        if(!sessioncheck) {
            error.add("ログインしてからタスク登録をしてください");
        }

        //入力漏れをチェック
        if(task_name.equals("")) {
            error.add("タスク名が空欄です");
        }
        boolean limit_check;
        limit_date_check = limit_date_check.replace('-', '/');
        DateFormat format = DateFormat.getDateInstance();
        format.setLenient(false);
        try {
            format.parse(limit_date_check);
             limit_check = true;
        } catch(Exception e) {
            limit_check = false;
        }
        if(!limit_check) {
            error.add("期限には日付を入力してください");
        }

        request.setAttribute("error", error);

        if(task_name != "") {
            try {
                //パラメータ通りに受け取れないもの、つまりカテゴリーと期限(String型でないもの)を変換
                int category_id = Integer.parseInt(category_id_str);
                Date limit_date = Date.valueOf(request.getParameter("limit_date"));

                //現在のversionとレコード取得時のversionを照合
                int current_version = taskdao.getVersion(task_id);
                if(current_version != version) {
                    error.add("このタスクは他の人によって改編されています");
                    request.setAttribute("error", error);
                    RequestDispatcher rd = request.getRequestDispatcher("task-update-failure.jsp");
                    rd.forward(request, response);
                }

                //タスクオブジェクトへ値を設定
                TaskBean task = new TaskBean();
                task.setTask_name(task_name);
                task.setCategory_id(category_id);
                task.setLimit_date(limit_date);
                task.setStatus_code(status_code);
                task.setMemo(memo);
                task.setTask_id(task_id);
                task.setVersion(version);

                //updateメソッドによりデータベース処理
                taskdao.updateTask(task);

                //sessionスコープへtaskを保存
                session.setAttribute("task", task);

                //リクエストスコープへカテゴリ名とステータス名を保存
                String category_name = categorydao.getCategoryName(task.getCategory_id());
                String status_name = statusdao.getStatusName(task.getStatus_code());
                request.setAttribute("category_name", category_name);
                request.setAttribute("status_name", status_name);

                RequestDispatcher rd = request.getRequestDispatcher("task-update-comp.jsp");
                rd.forward(request, response);
            } catch(SQLException | ClassNotFoundException | IllegalArgumentException e) {
                RequestDispatcher rd = request.getRequestDispatcher("task-update-failure.jsp");
                rd.forward(request, response);
            }
        }else {
            RequestDispatcher rd = request.getRequestDispatcher("task-update-failure.jsp");
            rd.forward(request, response);
            }
    }

次回予告

今回は、タスクの編集機能を実装しました。
次回はタスクの削除機能を実装します。

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

Dockerでのエラー解消に繋がったコマンド

はじめに

Docker環境構築において、役に立ったコマンドを備忘録として残します。
作成したものの、エラーが解決出来なかったときにおすすめです。

コンテナの停止、削除

コンテナの停止

コンテナ確認
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
797b4265f3d2        try_web             "/bin/sh -c 'rm -f t…"   24 minutes ago      Up 23 minutes       0.0.0.0:3000->3000/tcp              try_web_1

CONTAINER IDを指定します。

コンテナ停止
$ sudo docker stop 797b4265f3d2
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES

コンテナの削除

あくまで停止しただけなので、削除も行います。

コンテナ確認
$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
797b4265f3d2        try_web             "/bin/sh -c 'rm -f t…"   24 minutes ago      Up 23 minutes       0.0.0.0:3000->3000/tcp              try_web_1

CONTAINER IDを指定します。

コンテナ削除
$ sudo docker rm 797b4265f3d2
$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES

imageの削除

コンテナは削除しましたが、imageは残っています。

imageの確認
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
try_web             latest              c0ee642ca6bd        About an hour ago   1.01GB

IMAGE IDを指定します。

imageの削除
$ sudo docker rmi c0ee642ca6bd
$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

docker-compose exec app rails db:createのエラー

私の場合ですが、Access denied for user のエラーが出ていたため、
mysqlを確認しました。
# の後にmysql -u root -pと記述し、mysqlにログインします。

ターミナル
$ docker-compose up -d
$ docker-compose exec db bash
root@e5504121a08b:/# mysql -u root -p

終了する場合はexitと記述してください。

ここでパスワードが間違っていたため、
docker-compose exec app rails db:create
のエラーが出ていました。

まとめ

あくまでも一例のため、まだまだ便利なコマンドはあると思います。
私と同じ状況になった方の助けになれば幸いです。

またtwitterではQiitaにはアップしていない技術や考え方もアップしていますので、
よければフォローして頂けると嬉しいです。
詳しくはこちら https://twitter.com/japwork

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

MySQL応用編 いろんなSQL Vol.4

前回の記事で投稿した いろんなSQL Vol.3 のつづきの記事となります。


環境
 Windows 10
 MySQL : version(5.7.28)
Windows PowerShell : version(5.1.18362.1110)


21 TRIGGER

とあるテーブルで何らかの処理が起きた時に、それをトリガー(きっかけ)として何らかの処理をすることができる、という仕組みがTRIGGER

例 投稿一覧テーブルが更新されたら、ログ一覧テーブルに id, メッセージ, 作成日時 のログが残るようにする。

mysql> DROP TABLE IF EXISTS コメント一覧;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> DROP TABLE IF EXISTS 投稿一覧;
Query OK, 0 rows affected (0.02 sec)

mysql> DROP TABLE IF EXISTS ログ一覧;
Query OK, 0 rows affected (0.01 sec)

mysql> DROP TRIGGER IF EXISTS 投稿の更新;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> CREATE TABLE 投稿一覧 (
    ->   id INT NOT NULL AUTO_INCREMENT,
    ->   メッセージ VARCHAR(140),
    ->   PRIMARY KEY (id)
    -> );
Query OK, 0 rows affected (0.02 sec)

mysql> CREATE TABLE ログ一覧 (
    ->   id INT NOT NULL AUTO_INCREMENT,
    ->   メッセージ VARCHAR(140),
    ->   作成日時 DATETIME DEFAULT NOW(),
    ->   PRIMARY KEY (id)
    -> );
Query OK, 0 rows affected (0.02 sec)

mysql> CREATE TRIGGER
    ->   投稿の更新 -- triggerの名前を付ける
    -> AFTER UPDATE ON
    ->   投稿一覧 -- どのタイミングでどのような処理をするかを記述(投稿一覧が更新されたあとに)
    -> FOR EACH ROW
    ->   INSERT INTO
    ->     ログ一覧 (メッセージ)
    ->   VALUES
    ->     ('更新されたよ!');
-- 更新されたひとつひとつの行に対して、ログ一覧テーブルにデータを挿入できるようにする
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO 投稿一覧 (メッセージ) VALUES
    ->   ('はじめまして'),
    ->   ('春になったら行きたいところ'),
    ->   ('ドライブスポット教えて');
Query OK, 3 rows affected (0.01 sec)

mysql> UPDATE 投稿一覧 SET メッセージ = 'はじめまして!よろしくおねがいします!' WHERE id = 1;
 Query OK, 1 row affected (0.01 sec)


mysql> SELECT * FROM 投稿一覧;
+----+---------------------------------------------+
| id | メッセージ                                   |
+----+---------------------------------------------+
|  1 | はじめまして!よろしくおねがいします!         |
|  2 | 春になったら行きたいところ                    |
|  3 | ドライブスポット教えて                        |
+----+---------------------------------------------+
3 rows in set (0.00 sec)

mysql> SELECT * FROM ログ一覧;
+----+-----------------------+---------------------+
| id | メッセージ             | 作成日時            |
+----+-----------------------+---------------------+
|  1 | 更新されたよ!         | 2020-10-12 10:45:47 |
+----+-----------------------+---------------------+
1 row in set (0.00 sec)

補足

  • トリガーはUPDATE以外に、INSERTDELETEのタイミングでも使える.
  • AFTERではなくBEFOREとすれば処理前にトリガーを実行することができる。

例えば、
CREATE TRIGGER
トリガー名
BEFORE UPDATE ON

のように書くと、「更新前にトリガーを実行する」という命令にすることができる。

22 設定されているTRIGGERの一覧を確認

SHOW TRIGGERS;

mysql> SHOW TRIGGERS;
+-----------------+--------+--------------+---------------------------------------------------------------------------------------+--------+------------------------+----------------------------------------------------------------+----------------+----------------------+----------------------+--------------------+
| Trigger         | Event  | Table        | Statement                                                                             | Timing | Created                | sql_mode                                                       | Definer        | character_set_client | collation_connection | Database Collation |
+-----------------+--------+--------------+---------------------------------------------------------------------------------------+--------+------------------------+----------------------------------------------------------------+----------------+----------------------+----------------------+--------------------+
| 投稿の更新      | UPDATE | 投稿一覧     | INSERT INTO
ログ一覧 (メッセージ)
  VALUES
('更新されたよ!')                 | AFTER  | 2020-10-12 10:45:47.83 | STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | root@localhost | utf8                 | utf8_general_ci      | utf8_general_ci    |
+-----------------+--------+--------------+---------------------------------------------------------------------------------------+--------+------------------------+----------------------------------------------------------------+----------------+----------------------+----------------------+--------------------+
1 row in set (0.01 sec)

;\Gに変えて実行すると、縦表示になって見やすくなる

mysql> SHOW TRIGGERS\G
*************************** 1. row ***************************
         Trigger: 投稿の更新
           Event: UPDATE
           Table: 投稿一覧
       Statement: INSERT INTO
ログ一覧 (メッセージ)
  VALUES
('更新されたよ!')
          Timing: AFTER
         Created: 2020-10-12 10:45:47.83
        sql_mode: STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
         Definer: root@localhost
character_set_client: utf8
collation_connection: utf8_general_ci
  Database Collation: utf8_general_ci

23 外部ファイルの読み込み

data.csvというデータが入ったファイル(今回はcsvファイル)を用意して、それをMySQLで読み込みします。

LOAD DATA LOCAL INFILE 'ファイル名もしくはファイルのパス' INTO TABLE データを読み込む先のテーブル名
-- 以下、オプションを記述
FIELDS TERMINATED BY ',' -- 項目の区切りを指定(今回は',')
LINES TERMINATED BY '\n' -- 行の区切りを指定(今回は改行で'\n'を使用している)
IGNORE 1 LINES -- データの1行目を挿入したくない場合
(メッセージ,いいね,地域); -- データをフィールドに挿入する順番を指定

mysql> LOAD DATA LOCAL INFILE 'C:/Users/xxxx/data.csv' INTO TABLE 投稿一覧
    ->   FIELDS TERMINATED BY ','
    ->   LINES TERMINATED BY '\n'
    ->   LINES TERMINATED BY '\n'
    ->   IGNORE 1 LINES
    ->   (メッセージ,いいね,地域);
Query OK, 48 rows affected, 1 warning (0.01 sec)

mysql> SELECT * FROM 投稿一覧;
+----+----------------------------------------+-----------+----------+
| id | メッセージ                             | いいね    | 地域       |
+----+----------------------------------------+-----------+----------+
       |よろしくおねがいします                 |       212 | 徳島県
       |はじめまして                          |        46 | 兵庫県
       |フォローさせてもらいます               |         9 | 鹿児島県
       |はじめまして                          |       234 | 新潟県
       |はじめまして                          |       777 | 富山県
       |よろしくおねがいします                 |        98 | 鳥取県
       |はじめまして                          |       187 | 京都府
       |フォローさせてもらいます               |         4 | 佐賀県
       |フォローさせてもらいます               |       314 | 高知県
       |リプください                          |         2 | 秋田県
       |いいねください                        |        78 | 山形県
       |フォローありがとう                     |        99 | 埼玉県
       |フォローさせてもらいます               |       534 | 福岡県
       |よろしくおねがいします                 |        12 | 奈良県
       |フォローして                          |         9 | 群馬県   
       |今日の天気は                          |       312 | 東京都
       |はじめまして                          |        65 | 岐阜県
       |フォローさせてもらいます               |        34 | 愛媛県
       |よろしくおねがいします                 |       712 | 山口県
       |はじめまして                          |        37 | 大阪府
       |相互フォローよろ                      |       123 | 千葉県
       |リツイート求む                        |         5 | 茨城県
       |いいね                               |        98 | 福島県
       |ヒマ人集合                           |        23 | 宮城県
       |フォローさせてもらいます              |        84 | 沖縄県
       |はじめまして                         |        39 | 静岡県
       |フォローさせてもらいます              |       454 | 宮崎県
       |よろしくおねがいします                |       412 | 広島県
       |フォローさせてもらいます              |        24 | 長崎県
       |はじめまして                         |       231 | 石川県
       |よろしくおねがいします                |       124 | 島根県
       |はじめまして                         |       141 | 愛知県
       |はじめまして                         |        44 | 山梨県
       |はじめまして                         |        12 | 北海道
       |よろしくおねがいします                |       132 | 岡山県
       |はじめまして                         |       104 | 滋賀県
       |仲良くしてね                         |         1 | 岩手県
       |はじめまして                         |        34 | 福井県
       |今なにしてる                         |        13 | 神奈川県
       |フォローさせてもらいます              |        82 | 熊本県
       |よろしく                             |       123 | 青森県
       |リツイートありがとう                  |        10 | 栃木県
       |フォローさせてもらいます              |        34 | 香川県
       |はじめまして                         |        87 | 三重県
       |フォローさせてもらいます              |        12 | 大分県
       |はじめまして                         |        54 | 長野県
       |よろしくおねがいします                |        12 | 和歌山県
+------+------------------------------------+-----------+---------+

48 rows in set (0.00 sec)

 注意点

外部ファイルの読み込みを試していた時に出たエラーと、その解決方法をご紹介
1つめのエラー

mysql> LOAD DATA LOCAL INFILE 'data.csv' INTO TABLE 投稿一覧
    ->   FIELDS TERMINATED BY ','
    ->   LINES TERMINATED BY '\n'
    ->   (メッセージ,いいね,地域);
ERROR 2 (HY000): File 'data.csv' not found (Errcode: 2 - No such file or directory)

エラー内容

  • 使用しているデータベースのデータが保存されているディレクトリC:\ProgramData\MySQL\MySQL Server 5.7\Data\shop03を探して、同じディレクトリ内にcsvファイルを保存したのですが、ファイルの読み込みができなかった。

解決方法

  • 別のディレクトリにcsvファイルを移し、そのディレクトリの絶対パスを指定することで正常に読み込みができた。

2つめのエラー

mysql> LOAD DATA LOCAL INFILE 'C:/Users/easto/data.csv' INTO TABLE 投稿一覧
    ->   FIELDS TERMINATED BY ','
    ->   LINES TERMINATED BY '\n'
    ->   (メッセージ,いいね,地域);
ERROR 1300 (HY000): Invalid utf8 character string: '"'

エラー内容
読み込もうとしてファイルの文字コードがutf8じゃないから読み込めない

解決方法

  • 読み込みたいファイルを選択子て、右クリックする
    スクリーンショット 2020-10-13 101641.jpg

  • プログラムから開く(H)を選択し、メモ帳で開く
    スクリーンショット 2020-10-13 101739.jpg

  • ファイル(F)をタップし、名前を付けて保存(A)を選択したら、文字コード(E)をUTF-8に変更して保存(S)する
    スクリーンショット 2020-10-13 101838.jpg

これで文字コードの変更は完了です。
これが文字コードを変更する際の一番簡単な方法だと思います。

24 INDEX

よく検索されるカラムにインデックスをつける(=索引みたいなもの)

メリット
あらかじめデータを整列させておくことで、コンピューターが大量のレコードの中から目的のデータにたどり着くまでの時間を大幅に短縮することができるので、データの抽出が早くになる

デメリット
データの挿入、更新、削除をすると整列していたデータが崩れるので、インデックスのデータをいちいち再構築する手間が発生する。
また、インデックスを作る分データベースに必要な容量が増えてしまう。

補足
主キーに関してはPRIMARYというインデックスが自動的に作られるので、主キーを付けて作成したテーブルではidを使った検索はすでに高速に動作することができる。

インデックスの設定を確認
SHOW INDEX FROM テーブル名;

mysql> SHOW INDEX FROM 投稿一覧\G
*************************** 1. row ***************************
        Table: 投稿一覧
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: id
    Collation: A
  Cardinality: 47
     Sub_part: NULL
       Packed: NULL
         Null:
   Index_type: BTREE
      Comment:
Index_comment:

Column_name: idでidのフィールドにKey_name:でPRIMARYというINDEXが設定されていることがわかります。

idを使った検索で、Key_name:で設定されているINDEXが実際に使われているかどうかを調べてみましょう。

下記のように、SELECT文でidを使った検索のクエリを書き、先頭にEXPLAINを付けてあげます。

EXPLAIN SELECT * FROM テーブル名 WHERE id = 値\G

mysql> EXPLAIN SELECT * FROM 投稿一覧 WHERE id = 2\G
*************************** 1. row ***************************
       id: 1
  select_type: SIMPLE
    table: 投稿一覧
   partitions: NULL
     type: const
possible_keys: PRIMARY
      key: PRIMARY
  key_len: 4
      ref: const
     rows: 1
 filtered: 100.00
    Extra: NULL

key:で実際に使われたINDEXの名前が、
rows:で検索対象となるレコード数の見積りがわかります。

25 INDEXが設定されていない場合

mysql> EXPLAIN SELECT * FROM 投稿一覧 WHERE 地域  = '山口県'\G
*************************** 1. row ***************************
       id: 1
  select_type: SIMPLE
    table: 投稿一覧
   partitions: NULL
     type: ALL
possible_keys: NULL
      key: NULL
  key_len: NULL
      ref: NULL
     rows: 47
 filtered: 10.00
    Extra: Using where

key:を見るとNULLとなっているので、INDEXが設定されていないことがわかります。
また、rows: 47となっていることから47件、最初から最後まですべてのデータを確認した返してきた結果ということがわかります。

INDEXの設定方法

CREATE TABLE内に記述して設定することもできるが、あとから付け外しすることが多いのでALTER TABLE文を書いて設定していく方がより使いやすくなる。
ALTER TABLE テーブル名 ADD INDEX インデックス名(インデックスを指定したいカラム名);

mysql> ALTER TABLE 投稿一覧 ADD INDEX area_index(地域);
Query OK, 0 rows affected (0.08 sec)

mysql> SHOW INDEX FROM 投稿一覧\G
*************************** 1. row ***************************
    Table: 投稿一覧
   Non_unique: 0
 Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: id
Collation: A
  Cardinality: 47
 Sub_part: NULL
   Packed: NULL
     Null:
   Index_type: BTREE
  Comment:
Index_comment:
*************************** 2. row ***************************
    Table: 投稿一覧
   Non_unique: 1
 Key_name: area_index
 Seq_in_index: 1
  Column_name: 地域
Collation: A
  Cardinality: 47
 Sub_part: NULL
   Packed: NULL
     Null: YES
   Index_type: BTREE
  Comment:
Index_comment:

INDEX設定後、EXPLAINで確認

mysql> EXPLAIN SELECT * FROM 投稿一覧 WHERE 地域 = '山口県'\G
*************************** 1. row ***************************
       id: 1
  select_type: SIMPLE
    table: 投稿一覧
   partitions: NULL
     type: ref
possible_keys: area_index
      key: area_index
  key_len: 63
      ref: const
     rows: 1
 filtered: 100.00
    Extra: NULL

key: area_indexで、設定したINDEXがちゃんと使われており,
rows: 1で、検索が高速になっていることがわかります。

26 INDEXを外す方法

ALTER TABLE テーブル名 DROP INDEX 外したいインデックス名;

mysql> ALTER TABLE 投稿一覧 DROP INDEX area_index;
Query OK, 0 rows affected (0.02 sec)

mysql> SHOW INDEX FROM 投稿一覧\G
*************************** 1. row ***************************
    Table: 投稿一覧
   Non_unique: 0
 Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: id
Collation: A
  Cardinality: 47
 Sub_part: NULL
   Packed: NULL
     Null:
   Index_type: BTREE
  Comment:
Index_comment:
1 row in set (0.00 sec)

SHOW INDEXで確認してみてもarea_indexは表示されないので、ちゃんと外れていることがわかります。

27 WARNING

SQLを実行していると、
1 row in set, 1 warning (0.00 sec)
という一文にちょいちょい出くわして、「warningってなんだか恐ろしい
!」ってなったので少し調べてみました。

mysql> EXPLAIN SELECT * FROM 投稿一覧 WHERE 地域  = '山口県'\G
*************************** 1. row ***************************
       id: 1
  select_type: SIMPLE
    table: 投稿一覧
   partitions: NULL
     type: ALL
possible_keys: NULL
      key: NULL
  key_len: NULL
      ref: NULL
     rows: 47
 filtered: 10.00
    Extra: Using where
1 row in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `shop03`.`投稿一覧`.`id` AS `id`,`shop03`.`投稿一覧`.`メッセージ` AS `メッセージ`,`shop03`.`投稿一覧`.`いいね` AS `いいね`,`shop03`.`投稿一覧`.`地域` AS `地域` from `shop03`.`投稿一覧` where (`shop03`.` 投稿一覧`.`地域` = '山口県')
1 row in set (0.00 sec)

SHOW WARNINGSコマンドで、直前に実行したSQLのwarningの内容を確認することができます。(別コマンドを実行するとwarningの内容が上書きされるので注意が必要です。)

おわりに

4回に分けてやってきましたが、知れば知るほどにSQLの奥深さを知る結果となった気がします。
ただ、MySQLでできることが増えた嬉しさと楽しさを知りました!SQLが使えるようになると、色んなことができるし、SQLができると一生飯が食えるスキルになるという話も聞いたりしているので、引き続き勉強していきたいと思います。

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

Flask-AdminとFlask-LoginによるDB管理者画面の実装

はじめに

Flaskを使ったDjangoの管理者画面のようなものを実装するには、Flask-Adminというライブラリを使うと便利です。
しかしFlask-Adminをそのまま使うだけでは、パスワードを打つことなく(ログインすることなく)管理者画面に入ることができてしまい、セキュリティ上とても脆弱です。
この記事では、Flask-AdminとFlask-Loginを使用してログイン機能のついたDB管理者画面の実装を行っていきます。

この記事は

参考元

の情報量が若干少ないので、日本語でもう少し解説の情報を増やしてみた記事です。
英語のできる方や冗長な言い回しが苦手な方は、上記のリンクから参考元サイトを見ることができます。

MVCについて

Model-View-Controllerモデルについてほんの少しで良いので知っておく必要があります。
なぜならこの記事でモデルとかコントローラーとかの単語を出すからです。
↓の記事とかが参考になると思います。
MVCモデルについて

筆者の環境

Ubuntu20.04LTS
MySQL 8.0.21
Python3.8.5

Flask==1.1.2
Flask-Admin==1.5.6
Flask-Login==0.5.0
Flask-SQLAlchemy==2.4.4
mysqlclient==2.0.1

ソースコード全体

いきなりですが、お時間がない方用に最終的なソースコード全体をお見せします。
詳しい解説はこれ以降。

from flask import Flask, abort, jsonify, render_template, request, redirect, url_for
from wtforms import form, fields, validators
import flask_admin as admin
import flask_login as login
from flask_admin.contrib import sqla
from flask_admin import helpers, expose
from flask_admin.contrib.sqla import ModelView
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
  'user': os.environ['RDS_USER'],
  'password': os.environ['RDS_PASS'],
  'host': os.environ['RDS_HOST'],
  'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)

class AdminUser(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  login = db.Column(db.String(50), unique=True)
  password = db.Column(db.String(250))

  @property
  def is_authenticated(self):
    return True

  @property
  def is_active(self):
    return True

  @property
  def is_anonymous(self):
    return False

  def get_id(self):
    return self.id

  def __unicode__(self):
    return self.username


class LoginForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    user = self.get_user()

    if user is None:
      raise validators.ValidationError('ユーザー名もしくはパスワードが違います。')

    if not check_password_hash(user.password, self.password.data):
      raise validators.ValidationError('ユーザー名もしくはパスワードが違います。')

  def get_user(self):
    return db.session.query(AdminUser).filter_by(login=self.login.data).first()


class RegistrationForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
      raise validators.ValidationError('同じユーザー名が存在します。')


def init_login():
  login_manager = login.LoginManager()
  login_manager.init_app(app)

  @login_manager.user_loader
  def load_user(user_id):
    return db.session.query(AdminUser).get(user_id)


class MyModelView(sqla.ModelView):
  def is_accessible(self):
    return login.current_user.is_authenticated


class MyAdminIndexView(admin.AdminIndexView):
  @expose('/')
  def index(self):
    if not login.current_user.is_authenticated:
      return redirect(url_for('.login_view'))
    return super(MyAdminIndexView, self).index()

  @expose('/login/', methods=('GET', 'POST'))
  def login_view(self):
    form = LoginForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = form.get_user()
      login.login_user(user)

    if login.current_user.is_authenticated:
      return redirect(url_for('.index'))
    link = '<p>アカウント未作成用 <a href="' + url_for('.register_view') + '">ここをクリック</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/register/', methods=('GET', 'POST'))
  def register_view(self):
    form = RegistrationForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = AdminUser()

      form.populate_obj(user)
      user.password = generate_password_hash(form.password.data)
      db.session.add(user)
      db.session.commit()
      login.login_user(user)
      return redirect(url_for('.index'))
    link = '<p>既にアカウントを持っている場合は <a href="' + url_for('.login_view') + '">ここをクリックしてログイン</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/logout/')
  def logout_view(self):
    login.logout_user()
    return redirect(url_for('.index'))


init_login()
admin = admin.Admin(app, '管理者画面', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))


@app.route("/", methods=['GET'])
def index():
  return "Hello, World!"

if __name__ == "__main__":
  app.run()

DBに接続する

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
  'user': os.environ['RDS_USER'],
  'password': os.environ['RDS_PASS'],
  'host': os.environ['RDS_HOST'],
  'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)

ここの解説はインターネット上に日本語の記事も多いので省略します。

管理者アカウントのモデルを作成する

class AdminUser(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  login = db.Column(db.String(50), unique=True)
  password = db.Column(db.String(250))

  @property
  def is_authenticated(self):
    return True

  @property
  def is_active(self):
    return True

  @property
  def is_anonymous(self):
    return False

  def get_id(self):
    return self.id

  def __unicode__(self):
    return self.username

loginはユーザー名のことです。
管理者画面にログインする際のユーザー名とパスワードを定義しています。
各メソッドにpropertyデコレータが付いてるのは、後にログイン処理を書いていくときにログイン済かどうかとかそういう情報を取得するためです。
propertyデコレータについて詳しく知りたい方は↓
プロパティ

コントローラーの作成

class LoginForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    user = self.get_user()

    if user is None:
      raise validators.ValidationError('ユーザー名もしくはパスワードが違います。')

    if not check_password_hash(user.password, self.password.data):
      raise validators.ValidationError('ユーザー名もしくはパスワードが違います。')

  def get_user(self):
    return db.session.query(AdminUser).filter_by(login=self.login.data).first()


class RegistrationForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
      raise validators.ValidationError('同じユーザー名が存在します。')

ビュー(ログイン画面とか管理者アカウント登録画面)のフォームから入力を受けた際の処理を書いたコントローラーです。
ここで注目してほしいのは、LoginFormクラスにある

check_password_hash(user.password, self.password.data)

です。これはハッシュ化されて保存してある本当のパスワードと、ログイン画面から入力された値をハッシュ化したものを、両者比較し一致したらTrueを返しれくれる便利なやつです。

推奨されないですが、もしDBにパスワードを平文で保存している時は条件式のところを

if user.password != self.password.data:

に変更すると良いと思います。

ビューの作成

def init_login():
  login_manager = login.LoginManager()
  login_manager.init_app(app)

  @login_manager.user_loader
  def load_user(user_id):
    return db.session.query(AdminUser).get(user_id)


class MyModelView(sqla.ModelView):
  def is_accessible(self):
    return login.current_user.is_authenticated


class MyAdminIndexView(admin.AdminIndexView):
  @expose('/')
  def index(self):
    if not login.current_user.is_authenticated:
      return redirect(url_for('.login_view'))
    return super(MyAdminIndexView, self).index()

  @expose('/login/', methods=('GET', 'POST'))
  def login_view(self):
    form = LoginForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = form.get_user()
      login.login_user(user)

    if login.current_user.is_authenticated:
      return redirect(url_for('.index'))
    link = '<p>アカウント未作成用 <a href="' + url_for('.register_view') + '">ここをクリック</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/register/', methods=('GET', 'POST'))
  def register_view(self):
    form = RegistrationForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = AdminUser()

      form.populate_obj(user)
      user.password = generate_password_hash(form.password.data)
      db.session.add(user)
      db.session.commit()
      login.login_user(user)
      return redirect(url_for('.index'))
    link = '<p>既にアカウントを持っている場合は <a href="' + url_for('.login_view') + '">ここをクリックしてログイン</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/logout/')
  def logout_view(self):
    login.logout_user()
    return redirect(url_for('.index'))

init_login()
admin = admin.Admin(app, '管理者画面', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))

普通にFlaskでやる時と若干似てる感じですね。

ここで注目してほしいのはMyModelViewクラスです。
MyModelViewはsqla.ModelViewを継承し、is_accessibleメソッドをオーバライドしています。(する必要があるのです)
is_accessibleメソッドでは、ユーザーが既にログイン済みか否かを返しています。
is_accessibleメソッドをオーバライドするだけで、後のビュークラス(ここではMyAdminIndexViewクラス)でアクセス制御ルールを定義できるようになります。

init_login()
admin = admin.Admin(app, '管理者画面', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))

で実際にどのモデルにおいてどのビュークラスを使用するかなどを定義しています。

HTMLを書く

HTMLがないと意味がないですね。
プロジェクトルートディレクトリにtemplatesディレクトリを作成し、以下のような構造でファイルやディレクトリを作ります。

templates/
    admin/
        index.html
    my_master.html
    index.html

my_master.html

{% extends 'admin/base.html' %}

{% block access_control %}
{% if current_user.is_authenticated %}
<div class="btn-group pull-right">
  <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
    <i class="icon-user"></i> {{ current_user.login }} <span class="caret"></span>
  </a>
  <ul class="dropdown-menu">
    <li><a href="{{ url_for('admin.logout_view') }}">ログアウト</a></li>
  </ul>
</div>
{% endif %}
{% endblock %}

ログイン後の画面で、ユーザーIDのところを押されたらドロップダウンでログアウトボタンが出るようなやつです。

templates/index.html

<html>
  <body>
    <div>
      <a href="{{ url_for('admin.index') }}">Go to admin!</a>
    </div>
  </body>
</html>

インデックスページなんで何でも良いです。
適当に書いておきます。

templates/admin/index.html

{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="row-fluid">

    <div>
        {% if current_user.is_authenticated %}
        <h1>Civichat管理者画面</h1>
        <p class="lead">
            認証済
        </p>
        <p>
            データの管理はこちらの画面からできます。ログアウトしたい場合は/admin/logout にアクセスしてください。
        </p>
        {% else %}
        <form method="POST" action="">
            {{ form.hidden_tag() if form.hidden_tag }}
            {% for f in form if f.type != 'CSRFTokenField' %}
            <div>
            {{ f.label }}
            {{ f }}
            {% if f.errors %}
            <ul>
                {% for e in f.errors %}
                <li>{{ e }}</li>
                {% endfor %}
            </ul>
            {% endif %}
            </div>
            {% endfor %}
            <button class="btn" type="submit">完了</button>
        </form>
        {{ link | safe }}
        {% endif %}
    </div>

    <a class="btn btn-primary" href="/"><i class="icon-arrow-left icon-white"></i> 戻る</a>
</div>
{% endblock body %}

ログイン後の管理者画面のインデックスページのようなものです。

パスワード認証に加えIP制限したい

そもそもログインフォームにたどり着く前にIPアドレスでアクセス制限したい、というニーズもあると思います。
↓の記事が参考になると思います。

FlaskでIP制限する

最後に

何か間違いがあったらコメントでご指摘頂ければ幸いです。

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

MySQL ERROR! The server quit without updating PID file 解決方法

MySQLに悩まされたので調べたことまとめ

MySQLが起動できない、、、

sudo mysql.server start

動け、動け、うごくんだaaぁああ!!!

Starting MySQL
... ERROR! The server quit without updating PID file (/usr/local/var/mysql/MacBook-Pro.local.pid)

結論から

my.cnf というファイルに必要な設定が足りない

以下の一行を追加

/usr/local/etc/my.cnf
explicit_defaults_for_timestamp = 1

自分は/usr/local/etc/my.cnfにあった

その後MySQL関連のファイルを削除して再インストールするだけ

$ sudo rm -rf /usr/local/var/mysql
$ brew uninstall mysql@5.7
$ brew install mysql@5.7
パスを通す
echo 'export PATH="/usr/local/opt/mysql@5.7/bin:$PATH"' >> ~/.zshrc

.zshrcの設定の反映(忘れがち?)

$ source ~/.zshrc

インストールを確認

$ mysql --version                                                               ?[master]
mysql  Ver 14.14 Distrib 5.7.31, for osx10.15 (x86_64) using  EditLine wrapper

念願の?

$ mysql.server start                                                            ?[master]
Starting MySQL
 SUCCESS!

解決に至った道のり

とりあえずインストールしてみる

brew install mysql@5.7

インストールは成功したようだが、何やらWarningが、、、

Warning: The post-install step did not complete successfully
You can try again using `brew postinstall mysql@5.7`

指示通りに、以下コマンドを実行してみるもエラー?What the f**k!

$ brew postinstall mysql@5.7

...省略...

Warning: The post-install step did not complete successfully
You can try again using `brew postinstall mysql@5.7`

エラーはとりあえず「無視」して、MySQLを起動するがエラー

$ mysql.server start
ERROR! The server quit without updating PID file

だよね笑

要するにWarningだから大丈夫っしょって気軽な気持ちで無視していた、、、

おまけ(MySQL設定など)

MySQLに対する設定は、my.cnfというファイルに対して行う

my.cnfの場所と読み込む順番

長文の中に書かれている

$ mysql --help
...省略...

Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/local/etc/my.cnf ~/.my.cnf
The following groups are read: mysql client

...省略...

Macでの読み込み順番とmy.cnfの場所

1,/etc/my.cnf
2,/etc/mysql/my.cnf
3,/usr/local/etc/my.cnf
4,~/.my.cnf

my.cnfの場所と順番を確認する方法

grep結果2行目が場所と順番になる

$ mysql --help | grep my.cnf
                      order of preference, my.cnf, $MYSQL_TCP_PORT,
/etc/my.cnf /etc/mysql/my.cnf /usr/local/etc/my.cnf ~/.my.cnf

参考

MySQLの複数あるmy.cnfの場所全部を覚えず調べる方法
Home BrewでMySQL5.7をインストールしようとしてエラーではまった

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

DockerでRailsアプリの開発環境構築【Docker, Rails, Puma, Nginx, MySQL】

こんにちは.
今回もRailsアプリの開発環境をDockerで構築する手順をまとめてみました.
前回はRails用とデータベース用の2つのコンテナを起動しましたが,今回は,WebサーバーとしてNginxのコンテナも起動してみました.

事前準備

環境

Ruby: 2.5.8
Rails: 5.2.4.4
MySQL: 5.7.31
Nginx: 1.19.2
Docker: 19.03.12
Docker Compose: 1.26.2

手順

1. ディレクトリ,ファイルの作成

全体の構成は以下の通りです.
それではこの構成図の通り,ディレクトリとファイルを作成していきます.

全体の構成
/test-app
├── Dockerfile
├── Dockerfile.nginx
├── docker-compose.yml
├── nginx.conf
├── Gemfile
└── Gemfile.lock

まずは, プロジェクトのルートディレクトリを作成します.

terminal
$ mkdir test-app

そして, ルートディレクトリの直下に

  • Dockerfile
  • Dockerfile.nginx
  • docker-compose.yml
  • nginx.conf
  • Gemfile
  • Gemfile.lock

これらを作成します.

terminal
$ cd test-app
$ touch Dockerfile Dockerfile.nginx docker-compose.yml nginx.conf Gemfile Gemfile.lock 

2. ファイルの編集

上記で作成したそれぞれのファイルの中身は以下のようになります.
(Gemfile.lockは空のままにします.)

Dockerfile
FROM ruby:2.5
RUN apt-get update && apt-get install -y \
    build-essential \
    node.js
WORKDIR /test-app
COPY . /test-app
RUN bundle install
Dockerfile.nginx
FROM nginx
RUN rm -f /etc/nginx/conf.d/*
COPY nginx.conf /etc/nginx/conf.d/test-app.conf
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
containers/nginx/nginx.conf
# プロキシ先の指定
# Nginxが受け取ったリクエストをバックエンドのpumaに送信
upstream test-app {
  # ソケット通信したいのでpuma.sockを指定
  server unix:///test-app/tmp/sockets/puma.sock;
}

server {
  listen 80;
  # ドメインもしくはIPを指定
  server_name _;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  # ドキュメントルートの指定
  root /test-app/public;

  client_max_body_size 100m;
  error_page 404             /404.html;
  error_page 505 502 503 504 /500.html;
  try_files  $uri/index.html $uri @test-app;
  keepalive_timeout 5;

  # リバースプロキシ関連の設定
  location @test-app {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://test-app;
  }

  location /favicon {
    empty_gif;
    access_log    off;
    log_not_found off;
  }
}
Gemfile
source 'https://rubygems.org'
gem 'rails', '~>5.2'
docker-compose.yml
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    command: bundle exec puma -C config/puma.rb
    volumes:
      - .:/test-app
    tty: true
    stdin_open: true
    depends_on:
      - db

  db:
    image: mysql:5.7
    environment:
      - 'MYSQL_ROOT_PASSWORD=password'
    volumes:
      - 'db-data:/var/lib/mysql'

  web:
    build:
      context: .
      dockerfile: Dockerfile.nginx
    volumes:
      - ./public:/test-app/public
      - ./tmp:/test-app/tmp
    ports:
      - 80:80
    depends_on:
      - app

volumes:
  db-data:

3. Appのコンテナ内にRailsのセットアップを行う

terminal
$ docker-compose run --rm app rails new . --force --database=mysql --skip-bundle

4. tmp/socketsフォルダ作成,作成されたファイルを編集

まず,tmpフォルダ内にsocketsフォルダを作成します.

そして,Railsのセットアップにより作成されたファイルのうち以下の3つを編集します.
- config/database.yml
- config/puma.rb
- config/environments/production.rb

config/database.yml
default: &default
  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名と合わせる

development:
  <<: *default
  database: test-app_development
config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count

port        ENV.fetch("PORT") { 3000 }

environment ENV.fetch("RAILS_ENV") { "development" }

pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

plugin :tmp_restart

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"

stdout_redirect "#{app_root}/log/puma.stdout.log", "#{app_root}/log/puma.stderr.log", true
config/environments/production.rb
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = true   #デフォルトではfalseなので,trueにかえる

このconfig/environments/production.rbの変更はしなくても開発環境の構築については問題はないが,あとあと本番環境にデプロイした際にアセットプリコンパイルのエラーが出たので,ここで変更しています.

5. コンテナの起動, DBの作成

terminal
$ docker-compose up -d --build
$ docker-compose exec app rails db:create

これで http://localhost にアクセスすると, Railsのホーム画面が表示されるはずです.

参考

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

【続】tcpの仕様上、接続先がコネクションをcloseしているかはパケットを一度は実際に送るまでわからないよという話

はじめに

以前、tcpの仕様上、接続先がコネクションをcloseしているかはパケットを一度は実際に送るまでわからないよという話という記事をかいたのですが、そのきっかけは

以前、アプリからDBにSQLを投げたところ、コネクションがinvalidだというエラーが起きました。この原因自体はとても簡単でサーバ側(DB側)のコネクションを保持するタイムアウト設定がクライアントよりも短かったというだけなのですが、「これってクライアントライブラリ側でソケットにwriteした時点でエラーになるんだからハンドリングしてコネクションプールに保持している他のコネクションをよしなに使ってよ!!」と思ったのでした。

でした。

これはgoのmysqlドライバーを利用した場合に発生していたのですが、まさにこの問題をGitHubの中の人が去年に修正しており、それをテーマにブログを書かれていました。

それが非常に勉強になったので、わかりずらい部分を補足しつつ紹介したいと思います。

ブログを読む

背景

Three bugs in the Go MySQL Driverです。

背景とかの話も非常に興味深かったので若干主旨とずれる部分も紹介します。

GitHubのサービスはRailsのモノリスだったのを、ここ数年で少しずつ、速度や信頼性が必要な部分を中心に切り出していってGolangで書き換えていっているようです。

その中の一つのサービスが2019年に稼働したauthzdというサービスで、GitHub社のGoで書かれたWebアプリでMySQLに接続する初めてのサービスだったようです。

そのブログはその際に経験したバグに対してGitHub社が修正した3つのPRをもとに修正が紹介されています。今回は最初に紹介されているThe crashの部分を紹介します。

ちなみにresulting in our first “9” of availability for the serviceと書いてあったのでThe crashの修正によってサービス可用性90%突破したようです。

業務でサービス可用性目標をあげるところもあると思いますが、OSSがボトルネックになっていたのでOSSを修正するというのは素晴らしいですね!!

ちなみにブログに添付されているスクリーンショットはDatadogのmonitorのようなので、GitHub社もDatadogを利用しているようですね(どうでもいい)

The crash

どんな話なのかを先にざっくり書いてしまうと、MySQLのサーバ側のidle timeoutがクライアントのそれよりも短い場合、クライアントからクエリを送ろうとしたとき実はそのコネクションはサーバ側がcloseしていたということがおこりえます。その場合にクライアントとしては強制的にエラーになることを経験します。

この問題への対応としては簡単で(*DB).SetConnMaxLifetimeをサーバのidle timeoutより小さくすればいいだけです。
ただ、SetConnMaxLifetimeであってSetIdleConnMaxLifetimeではないので、idleではなくactiveなコネクションも不必要にcloseされてしまい、イケていないです。これは全てのDBサーバのコネクションにidleという概念があるわけではないため、database/sql側が用意していないという背景があるようです。

自分はまさに上記の対応を行って(参考までにDBのidle timeoutはAWSのAuroraの場合、デフォルトで8hに設定されているようです。GitHub社では30sに設定しているようです。短い!!)おり、そのときにmysqlドライバー側でよしなにできないの?と思い、調査したことを以前記事にしたわけですが、修正してくれたようです。

さて、詳細に入っていきます。

記事の序盤はtcpの仕様上、接続先がコネクションをcloseしているかはパケットを一度は実際に送るまでわからないよという話とほぼ同じことがTCPの遷移図とともに書かれています。

TCPの仕様上、サーバがFINパケットを送ってもそれはあくまでサーバ側がwriteしないことのみを意味し、クライアントからはサーバへwriteし、サーバがreadし処理をするのはありえます。そして、サーバがwriteもreadも全くなにもしない(例えばソケットをcloseするなど)ことを安全にクライアントへ伝える方法はtcpのプロトコルに存在しません。

わかりやすいので以下引用しますが、TCPの上記のような特性はほとんどのプロトコルの場合は問題にならないようですが、MySQLのプロトコルは「クライアントが送りサーバがそれに返答する」という流れが決まっており、クライアントはwriteするまでreadすることがないようです。

In most network protocols on top of TCP, this isn’t an issue. The client is performing reads from the server, and as soon as it receives a [SYN, ACK], the next read returns an EOF error, because the Kernel knows that the server won’t write more data to this connection. However, as discussed earlier, once a MySQL connection is in its Command Phase, the MySQL protocol is client-managed. The client only reads from the server after it sends a request, because the server only sends data in response to requests from the client.

そういえば、この特性はHTTP/1.x(pipeliningは除く)も同様かと思いますが、以前、Goのhttp.Requestのキャンセルの仕組みを理解するという記事で書いたようにGoのhttpサーバの実装ではリクエストボディを読み切ったタイミングでソケットをReadするgoルーチンが作成され、サーバ処理中にクライアント側のcloseに気づけるようになっています。こちらはサーバ側の話ですが。

ここまで話を聞いて、エラーだったらretryしてくれよと思う方もいるかもしれません。
実は、retryの仕組みはdatabase/sqlに用意されており、ErrBadConnを返却するようにすれば、maxBadConnRetries(2回)リトライし、それでもエラーになればコネクションプールを利用せずに新規のコネクションを作成する実装になっています。

以下はQueryContextの例ですが、database/sqlのあらゆる処理に同じようなリトライの処理があり、また、driver側(goのmysqlドライバーも)でもdatabase/sql/driverimportして、driver.ErrBadConnを返却しているケースがあるようです。

database/sql/driver/driver.go
// ErrBadConn should be returned by a driver to signal to the sql
// package that a driver.Conn is in a bad state (such as the server
// having earlier closed the connection) and the sql package should
// retry on a new connection.
//
// To prevent duplicate operations, ErrBadConn should NOT be returned
// if there's a possibility that the database server might have
// performed the operation. Even if the server sends back an error,
// you shouldn't return ErrBadConn.
var ErrBadConn = errors.New("driver: bad connection")
database/sql/sql.go
// QueryContext executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error) {
    var rows *Rows
    var err error
    for i := 0; i < maxBadConnRetries; i++ {
        rows, err = db.query(ctx, query, args, cachedOrNewConn)
        if err != driver.ErrBadConn {
            break
        }
    }
    if err == driver.ErrBadConn {
        return db.query(ctx, query, args, alwaysNewConn)
    }
    return rows, err
}

今回のも同じようにErrBadConnを返却するようにしていればそもそも問題にならない(リトライに失敗しても最後にはコネクションプールを必ず使わないから)のですが、エラーが発覚する箇所がwriteである(Goのhttpserver実装のようになんらかの仕組みを用意しない限りwriteで初めてサーバのcloseに気付く)ので、常に安全にリトライできないという事情があるようです。

ブログに紹介されている以下のケースは、まさにErrBadConnのコメントにあるTo prevent duplicate operations, ErrBadConn should NOT be returned if there's a possibility that the database server might have performed the operationのケースなので、ErrBadConnは返却してはいけないということになります。

What would happen if we performed an UPDATE in a perfectly healthy connection, MySQL executed it, and then our network went down before it could reply to us? The Go MySQL driver would also receive an EOF after a valid write. But if it were to return driver.ErrBadConn, database/sql would

では、writeするまえにnon-blockingでreadしてEOFであればErrBadConnをなげればいいのでは?

と思うかもしれませんが、まさにそれがPRで対応されていることです!

いやー、事情が複雑ですね。。

PRを読む

packets: Check connection liveness before writing queryを実際によんでいきましょう。
修正方針を前章で把握するのだけでもお腹いっぱいですが、PRも100行ほどの小さいPRにもかかわらず、なかなか勉強になりました。

勉強になった点を3つ紹介します。

チェックを行うときには生のファイルディスクリプタを参照する

やることは前章で整理したようにwriteする直前にソケットをnon-blockingでreadしてすでにサーバがclose済みであればErrBadConnを返すだけです。

が、Goのネットワーク処理はAPIとしては同期的なAPIを提供しているが、実は内部ではnon-blockingな処理がされています。

簡単に説明すると、netpollerと呼ばれる仕組みでネットワークの待ちになった際に、goroutineが元処理から切り離されepollなどのシステムコールで非同期にソケットに対するイベントを把握し、処理可能になったら再度goroutineを割り当てる仕組みがgoのランタイムには備わっています(といっても自分は該当部分のソースを読んだことがないです)

これは本当に素晴らしい仕組みだと思うのですが、今回のようにブロックされることがないことが確定している場合には生のファイルディスクリタを利用したシステムコールを使った方が好ましいです。というわけで以下のような実装がされています。

明示的にnon-blockingにしていないのは、生のファイルディスクリタはGoのランタイム側ですでにO_NONBLOCK指定されているためだと思います。

conncheck.go
    sconn, ok := c.(syscall.Conn)
    if !ok {
        return nil
    }
    rc, err := sconn.SyscallConn()
    if err != nil {
        return err
    }
    rerr := rc.Read(func(fd uintptr) bool {
        n, err = syscall.Read(int(fd), buff[:])
        return true
    })
    switch {
    case rerr != nil:
        return rerr
    case n == 0 && err == nil:
        return io.EOF
    case n > 0:
        return errUnexpectedRead
    case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
        return nil
    default:
        return err
    }

チェックを行う回数をできるだけ少なくする

ResetSessionsql/driver側でinterfaceで定義されており、この処理は処理済みのコネクションをコネクションプールに戻す時にsql/driverが呼びます。これにより実装するdriver側が処理を行う機会を得ます。

今回のPRではこのinterface実装でコネクションに設けたフラグをonにし、write時にこのフラグがあるときのみチェックを行い、チェック後フラグをoffにするという工夫がされています。

これにより、コネクションプールから取得したコネクションが最初に通信する時のみにチェックがされることになります。すごい!!

database/sql/driver/driver.go
// SessionResetter may be implemented by Conn to allow drivers to reset the
// session state associated with the connection and to signal a bad connection.
type SessionResetter interface {
    // ResetSession is called while a connection is in the connection
    // pool. No queries will run on this connection until this method returns.
    //
    // If the connection is bad this should return driver.ErrBadConn to prevent
    // the connection from being returned to the connection pool. Any other
    // error will be discarded.
    ResetSession(ctx context.Context) error
}

windowsでは何もしない

PRではwindowsで動作確認がとれておらず、CIも存在しない。どうやって動作確認しようか
と議論がつまりかけたのですが、// +build !windowsが指定されているconncheck.goconncheck_windows.goの両方のファイルでconnCheck関数を実装し、conncheck_windows.go側ではnilを返すというだけという技を使って議論を進めていました。これによってwindows側には何の変更もなしに修正したことになります。

すごい!!

おわりに

PRを確認すると最初にあげる時点でかなり詳しく説明し、パフォーマンス遅延などへの影響等も検証されていてすごいと思いました。OSSでPRあげる時はどうしても低姿勢になりがちな気がしますが、自分の修正内容に自信をもっていて、フローが遅いぞと圧をかけたり、こんな大変なissueも残っているんだと言われた場合もokそっちは来週PR作るねといって実際にmergeされているのでやばい

内容が素晴らしく、自分も努力せねばと思ったので紹介させていただきました。

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