- 投稿日:2020-10-13T23:51:53+09:00
MySql。OPTIMIZEで、DELETEでレコード削除した後のディスク領域を解放する方法
問題点。
MySqlのDELETEでレコードを大量に削除してもテーブルのディスク領域が解放されない。解決法。
OPTIMIZE TABLEというテーブルを最適化するのを実行するとディスク領域がちゃんと解放される。例。
テーブル名を"table_names"とする
SQL文は以下となるOPTIMIZE TABLE table_names
これでディスク領域が解放される。
MySqlのバージョンが5.6.5以前だと注意が必要らしい。
自分はバージョン5.7なので気にしない。
- 投稿日:2020-10-13T23:10:09+09:00
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. 各テーブルのデータ件数をチェックして下記になっていればOKmysql> 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.phpRoute::get('/', 'WelcomeController@index'); Route::resource('players', 'PlayerController'); Route::resource('countries', 'CountryController'); Route::resource('goals', 'GoalController'); Route::resource('pairings', 'PairingController');課題はここから…
- テーブルビューを1つ(あるいは2つ)追加
- functionを1つ追加
- controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします
答えはコチラ
Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストールイージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)
- 投稿日:2020-10-13T23:10:09+09:00
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. 各テーブルのデータ件数をチェックして下記になっていればOKmysql> 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.phpRoute::get('/', 'WelcomeController@index'); Route::resource('players', 'PlayerController'); Route::resource('countries', 'CountryController'); Route::resource('goals', 'GoalController'); Route::resource('pairings', 'PairingController');課題はここから…
- テーブルビューを1つ(あるいは2つ)追加
- functionを1つ追加
- controllerからviewに変数渡し してこのツイートと同じ見た目になるようにします
答えはコチラ
Task 5はいくつか追加の課題もあります
1. レコード追加
2. 論理削除
3. phpMyAdminのインストールイージーモードになるyps委員長のブログはこちら
miyupaca log ⇒ yps学習記録その5以上でTask5は終了です。
(ここでかなりの脱落者が出ましたが、今は答えもGitHubに載っているのでコピペでもいけてしまうかと思います…)
- 投稿日:2020-10-13T22:33:50+09:00
EC2上のmysqlにSequelProから接続してデータを挿入する
SequelProを使ってデータベースにデータを挿入する方法を備忘録として残します。
まずSequelProを開きましょう!するとこんな画面ですよね
左下の+ボタンを押す
お気に入りのところに新しく追加されます
名前はわかりやすいように自分で変えましょう標準にカーソルがあっているものをSSHに合わせる
ここのフォームに必要な情報を入力していくとデータベースにアクセスできます・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上で[コマンド]+[シフト]+[ドット]を打つと隠しディレクトリが現れる。
このなかにpemキーのファイルがあるので選択
この手順で本番環境のデータベース にアクセスできました。
- 投稿日:2020-10-13T21:16:34+09:00
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/mysqldockerfileに RUN npm install -g create-nuxt-app を追加
dockerfileFROM 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内で
tarminalroot@########:/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/
お疲れ様でした?
- 投稿日:2020-10-13T20:49:21+09:00
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.javaprotected 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.javaprotected 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); } }次回予告
今回は削除機能の実装をしてきました。
初めてのアプリケーションに最低限の機能はここまでの記事にあるコードを使えばできます。
次回は、排他制御と言う複数のユーザが同時にアプリケーションを使うことを考えた時の処理について紹介してまいります。
- 投稿日:2020-10-13T20:31:41+09:00
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.javaprotected 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.javaprotected 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); } }次回予告
今回は、タスクの編集機能を実装しました。
次回はタスクの削除機能を実装します。
- 投稿日:2020-10-13T20:22:40+09:00
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_1CONTAINER 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_1CONTAINER IDを指定します。
コンテナ削除$ sudo docker rm 797b4265f3d2 $ sudo docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESimageの削除
コンテナは削除しましたが、imageは残っています。
imageの確認$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE try_web latest c0ee642ca6bd About an hour ago 1.01GBIMAGE IDを指定します。
imageの削除$ sudo docker rmi c0ee642ca6bd $ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZEdocker-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
- 投稿日:2020-10-13T19:57:54+09:00
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
以外に、INSERT
やDELETE
のタイミングでも使える.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_ci23 外部ファイルの読み込み
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じゃないから読み込めない解決方法
これで文字コードの変更は完了です。
これが文字コードを変更する際の一番簡単な方法だと思います。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ができると一生飯が食えるスキルになるという話も聞いたりしているので、引き続き勉強していきたいと思います。
- 投稿日:2020-10-13T19:09:33+09:00
Flask-AdminとFlask-LoginによるDB管理者画面の実装
はじめに
Flaskを使ったDjangoの管理者画面のようなものを実装するには、Flask-Adminというライブラリを使うと便利です。
しかしFlask-Adminをそのまま使うだけでは、パスワードを打つことなく(ログインすることなく)管理者画面に入ることができてしまい、セキュリティ上とても脆弱です。
この記事では、Flask-AdminとFlask-Loginを使用してログイン機能のついたDB管理者画面の実装を行っていきます。この記事は
参考元
- Introduction To Flask-Admin
- This example shows how to integrate Flask-Login authentication with Flask-Admin using the SQLAlchemy backend.
の情報量が若干少ないので、日本語でもう少し解説の情報を増やしてみた記事です。
英語のできる方や冗長な言い回しが苦手な方は、上記のリンクから参考元サイトを見ることができます。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.usernameloginはユーザー名のことです。
管理者画面にログインする際のユーザー名とパスワードを定義しています。
各メソッドに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.htmlmy_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アドレスでアクセス制限したい、というニーズもあると思います。
↓の記事が参考になると思います。最後に
何か間違いがあったらコメントでご指摘頂ければ幸いです。
- 投稿日:2020-10-13T16:14:32+09:00
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.cnfexplicit_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をインストールしようとしてエラーではまった
- 投稿日:2020-10-13T10:20:00+09:00
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.lock2. ファイルの編集
上記で作成したそれぞれのファイルの中身は以下のようになります.
(Gemfile.lockは空のままにします.)DockerfileFROM ruby:2.5 RUN apt-get update && apt-get install -y \ build-essential \ node.js WORKDIR /test-app COPY . /test-app RUN bundle installDockerfile.nginxFROM 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.confcontainers/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; } }Gemfilesource 'https://rubygems.org' gem 'rails', '~>5.2'docker-compose.ymlversion: '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-bundle4. tmp/socketsフォルダ作成,作成されたファイルを編集
まず,tmpフォルダ内に
sockets
フォルダを作成します.そして,Railsのセットアップにより作成されたファイルのうち以下の3つを編集します.
-config/database.yml
-config/puma.rb
-config/environments/production.rb
config/database.ymldefault: &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_developmentconfig/puma.rbthreads_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", trueconfig/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のホーム画面が表示されるはずです.
参考
- 投稿日:2020-10-13T01:10:22+09:00
【続】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/driver
をimport
して、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.gosconn, 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 }チェックを行う回数をできるだけ少なくする
ResetSession
がsql/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.go
とconncheck_windows.go
の両方のファイルでconnCheck
関数を実装し、conncheck_windows.go
側ではnil
を返すというだけという技を使って議論を進めていました。これによってwindows側には何の変更もなしに修正したことになります。すごい!!
おわりに
PRを確認すると最初にあげる時点でかなり詳しく説明し、パフォーマンス遅延などへの影響等も検証されていてすごいと思いました。OSSでPRあげる時はどうしても低姿勢になりがちな気がしますが、自分の修正内容に自信をもっていて、フローが遅いぞと圧をかけたり、こんな大変なissueも残っているんだと言われた場合もokそっちは来週PR作るねといって実際にmergeされているのでやばい
内容が素晴らしく、自分も努力せねばと思ったので紹介させていただきました。