- 投稿日:2019-12-02T22:11:01+09:00
本番DBのデータを文字化けさせた話
実際にあったお話を供養させます。
何が原因だったのかを推測しながら読んでもらえるとポストモーテム冥利に尽きます。経緯
混入
- 画像コメントの文字数の上限を50文字→100文字へ増やしたいという要望が来ました。
- 対応者は下記のようなDDLで対応することにしました。
-- テーブルはシャーディングされている ALTER TABLE `table_1` CHANGE `image_comment` `image_comment` varchar(100) DEFAULT NULL; ALTER TABLE `table_2` CHANGE `image_comment` `image_comment` varchar(100) DEFAULT NULL; -- たくさんある… たくさん… ALTER TABLE `table_100` CHANGE `image_comment` `image_comment` varchar(100) DEFAULT NULL;
- 私はDBAのような役回りを担っており、「テスト環境でも動作確認して、問題ない」と判断しました。
- そうしてアップデート当日を迎え、上記のDDLを流しました。
- 無事に画像コメントの文字数上限が増えたことを本番環境で確認し、家路についたのです。
発覚
- 翌朝、いつもどおり出社して仕事をし始める私。
- しばらくすると上記の対応をしたスタッフがやってきて言うのです。
- 「なんか画像のコメントが文字化けしてるみたいなんですよね…」
- 「……?!」
調査
- そんな馬鹿な… という思いを捨てきれず調査開始
- 文字化けが発生しているという
table_60
を確認し始める…- 数分後、私は衝撃の事実に気付くのです。
「うわぁあ!!」と飛び上がって驚いたのは後にも先にもこのときくらいです。
ここまで
- 対象のテーブルはシャーディングされている
- 私が入社する前からたくさんあります
文字数を増やす対応をしただけなのに文字化けした…
- VARCHAR 型で文字化けに関わるオプションといえば?
- ちなみに変更したカラムの Before です
image_comment varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
テスト環境でも本番環境でも確認したのに…
- その「確認」は漏れてない?
答えに入る前に
今年のアドベントカレンダーは本番環境でやらかしちゃった人 Advent Calendar 2019 を要チェックしてます。
こういうやらかし系の共有は本当にありがたいです。
死屍累々のポストモーテム会…(゚A゚;)ゴクリ
惨劇はなぜおこってしまったのか
-- 文字化けしてないテーブル CREATE TABLE `table_1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', -- カラム省略 `image_comment` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=DYNAMIC;-- 文字化けしてるテーブル CREATE TABLE `table_60` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', -- カラム省略 `image_comment` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci ROW_FORMAT=DYNAMIC;あ!やせいの
latin1
があらわれた!何故か一部のテーブルだけ
CHARSET=utf8 COLLATE=utf8_unicode_ci
ではなかったのです!!なんでかわかりませんが、昔からそうだったようです…
つまりこうなります
元々入ってた文字データ(
CHARSET=utf8 COLLATE=utf8_unicode_ci
)が、カラムの型変更のタイミングで文字セットを指定しなかったため、テーブルのデフォルトCHARSET=latin1 COLLATE=latin1_swedish_ci
になる。
- するとどうなる?
- 文字が化ける
- するとどうなる?
- メンテが始まる
二度と惨劇を起こさないためにどうしたのか
- シャーディングされているテーブルに齟齬がないかの確認
- これは事件後に対応しました
- あなたの現場のシャーディングは、大丈夫ですか?
- 全テーブルでのデバッグ
- 登録済みのデータが無事か、全テーブルからいくつかサンプルでレコードを取ってくるようなデバッグをすればよかったです
- VATCHAR型, TEXT型の変更時には
CHARACTER SET utf8 COLLATE utf8_unicode_ci
あるいはCHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
を入れる
- これは今でもトラウマなのでチェックします
ひとこと
ポストモーテムを書くのも読むのもトラブルシューティング力を高めるきっかけになるかと思います!
みなさんももっと恥を忍んでさらけ出しましょう!参考リンク
- 投稿日:2019-12-02T21:04:37+09:00
MySQLで “xxx doesn’t have a default value”とエラーが表示されたときの対処法
MySQLで値を挿入する際、次のようなエラーが発生することがあります。
SQLSTATE[HY000]: General error: 1364 Field ‘xxxx’ doesn’t have a default valueこれは、挿入しようとしているテーブルのあるフィールドについて、値が指定されていない場合に発生します。MySQL 5.6以前のバージョンであれば問題なかったのですが、5.6から設定が一部変更されました。対処法を紹介しましょう。
SQLで値を指定する
このエラーメッセージは、値を指定すべきカラムに指定されていないことが問題であるため、SQL内で空の値などを指定することで回避することができます。
INSERT INTO xxx SET name=’abc’; ↓ INSERT INTO xxx SET name=’abc’, cnt=0;※ cntカラムでエラーが発生している場合
STRICT_TRANS_TABLESを外す
対処としては、「STRICT_TRANS_TABLES」という設定を変えることになります。MySQLの設定ファイル(my.cnf)を書き換えましょう。
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES ↓ sql_mode=NO_ENGINE_SUBSTITUTIONこうして、データベースを再起動すれば設定が有効になります。
XAMPPでの my.cnfの変更方法
XAMPPを利用している場合、my.cnfファイルはコントロールパネルから編集することができます。図のボタンをクリックし、「my.cnf」をクリックしましょう。
ただし、XAMPP 7.2に同梱されているデータベースは MariaDB(MySQLと互換性のある別製品)であるため、この問題の対象ではありません。
MAMPでの my.cnfの変更方法
MAMPを利用している場合は、以下に設定ファイルが存在しています。
アプリケーション→MAMP→conf→my.cnfただし、最初はファイル自体が存在しないため、その場合は新しくファイルを作成し、以下の一文を追加しましょう。
[mysqld] sql_mode=NO_ENGINE_SUBSTITUTIONこれにより、標準の設定が上書きされます。サーバーを再起動しましょう。
- 投稿日:2019-12-02T19:47:26+09:00
MySQL 8.0.18でパスワードリセットしてもrootユーザにログインできなくなった時の解決方法
目的
- 何をしてもMySQLのrootユーザにログインできなくなった時の解決方法を書く
結論
- MySQLをアンインストール→インストールしてrootユーザのパスワードを再登録した。
実施概要
- MySQLのファイル削除とアンインストールとインストール
- パスワードの再設定とログイン確認
実施詳細
MySQLのファイル削除とアンインストールとインストール
下記コマンドを実行してMySQLのファイルを削除する。(DB内のデータも消すことになるので注意!何もせず下記コマンドを実行するとデータベースに保存されている内容が全て吹き飛ぶ)
$ sudo rm -rf /usr/local/var/mysql下記コマンドを実行してMySQLをアンインストールする。
$ brew uninstall mysql下記コマンドを実行してMySQLをインストールする。
$ brew install mysqlパスワードの再設定とログイン確認
下記コマンドを実行してMySQLを起動する。
$ mysql.server restart下記コマンドを実行してMySQLに入る。
$ mysql -u root -p下記コマンドを実行してパスワードを設定する。
mysql>USE mysql; mysql>ALTER USER 'root'@'localhost' identified BY '任意のパスワード';下記コマンドを実行してMySQLを抜ける。
mysql>exit下記コマンドを実行後、パスワードの入力を求められるため入力してログインできるか確認する。
$ mysql -u root -p下記のようになればOKである。
mysql>あとはcreateしてmigrateすればテーブル構造は元どおりになる。
- 投稿日:2019-12-02T18:30:29+09:00
Node.js & Express & MySQL & React でTODOリスト Herokuにデプロイ編
はじめに
前回の続きです。
https://qiita.com/hcr1s/items/0e5970c5af496c221a24今回は、前回作成したAPIをHerokuへデプロイしていきます。
前提
今回は、Herokuのアカウントを所持しており、クレジット登録していることを前提とします。
Heroku app作成
まずは、git initやheroku createを実行していきます。
# 既にgit initしている場合は省略 $ git init $ git add . $ git commit -m 'commit名' # herokuでアプリ作成 $ heroku create アプリ名 $ heroku git:remote -a アプリ名 $ git push heroku masterとりあえず本番環境へpushは完了です。
MySQL
本番環境でMySQLへ接続するための設定を行なっていきます。
$ heroku addons:add cleardbHerokuでは、cleardbというクラウドサービスのMySQLを使用できます。
$ heroku config | grep CLEARDB_DATABASE_URL ↓ CLEARDB_DATABASE_URL: mysql://user名:password@host名/database名?reconnect=trueこのコマンドで、必要なデータベースの情報を取得できます。
この情報をconfigに設定していきます。$ heroku config:set DATABASE_URL='mysql://*********?reconnect=true'mysql://のあとは、grepの結果をそのままペースとしてください。
以上でcleardbの設定は終了です。
index.jsの編集
実際にプログラムに必要なコードを記述していきます。
まずは、前回も書いたcreateConnectionの編集をしていきます。index.js--- 省略 --- const databaseName = 'database名' const connection = mysql.createConnection({ host: host名, user: user名, password: 'password', database: 'database名' }) --- 省略 ---中身に関しては、MySQLの設定時のgrepを参考に記述していきます。
そして、必要なデータベースやテーブルが存在しない場合、つまり初回に限り実行されるSQLを記述します。index.jsconnection.query('create database if not exists ??;', databaseName) connection.query('use ??', databaseName) connection.query('create table if not exists todo_list(id int auto_increment, name varchar(255), isDone boolean, index(id))')前回も書きましたが、SQL文にフィールドを使用する際は??と記述します。
以上で、コードの編集も終了です。
package.jsonの編集
Herokuにデプロイした際に、package.jsonに記述がないとnpm startが実行されてしまうので編集します。
package.json{ "name": "todo-api", "version": "1.0.0", "description": "", "engines":{ "node": "12.13.0", "npm": "6.12.0" }, "main": "index.js", "scripts": { "start" : "node index.js" }, --- 省略 ---デプロイ
最後にデプロイをして終了です。
実際に使ってみましょう。
実際にPOSTを送ってみた際のスクショです。
いい感じですね。終わり
次回は、このAPIを使用してTODOアプリを開発していきます。
何か間違いがある際はおしらせください!
- 投稿日:2019-12-02T18:08:31+09:00
MySQLでクライアント(v8)からサーバー(v5.7)でのDumpファイル取得方法
# 実行 mysqldump --column-statistics=0 -u root -p -h [HOST] [DB] > dump.txt # エラー mysqldump: Couldn't execute 'SELECT COLUMN_NAME, JSON_EXTRACT(HISTOGRAM, '$."number-of-buckets-specified"') FROM information_schema.COLUMN_STATISTICS WHERE SCHEMA_NAME = '[DB]' AND TABLE_NAME = 'action_histories';' : Unknown table 'COLUMN_STATISTICS' in information_schema (1109)
--column-statistics=0
をパラメタ指定することで実行できるようになる`mysqldump --column-statistics=0 -u root -p -h [HOST] [DB] > dump.txt
- 投稿日:2019-12-02T17:55:41+09:00
MySQLで外部キー制約の一覧を取得する方法
mysqlselect * from `information_schema`.table_constraints where constraint_type="FOREIGN KEY";テーブルを削除しようとして外部キー制約で困った場合にどうぞ
- 投稿日:2019-12-02T17:49:45+09:00
AmazonLinux2 EC2インスタンス作成後、mysql: コマンドが見つかりません の状態からRDS接続するまで
環境
- AmazonLinux2(AWS EC2)
目的
AmazonLinux2 EC2インスタンス作成後、既存のRDSにmysqlコマンドを使ってデータベース接続したい。
つまづいたところ
EC2インスタンス作成後に「mysql: コマンドが見つかりません」というトラブルに遭遇。mysqlがインストールされていないようなのでインストールする。
しかし、mysqlのインストールを試みるものの、mysqlではなく下のようにmariadbをインストールしようとしてくる。$ sudo yum install mysql : : ======================================================================================================================== Package アーキテクチャー バージョン リポジトリー 容量 ======================================================================================================================== インストール中: mariadb x86_64 1:5.5.64-1.amzn2 amzn2-core 9.0 M解決方法
- mysql-communityリポジトリを使ってmysqlをyumインストールする。
以下、mysqlのインストール手順
MySQL Community Downloadsページよりダウンロードしたいmysql-communityリポジトリのパッケージを選択する。ここでは、「Red Hat Enterprise Linux 7 / Oracle Linux 7 (Architecture Independent), RPM Package」を指定。
↓
上記ページから移動した先のページに記載してあるダウンロードリンクをコピペしてmysql-communityリポジトリのインストールを実行。$ sudo yum install https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm↓
mysqlをインストールする。$ sudo yum install mysql : : ======================================================================================================================== Package アーキテクチャー バージョン リポジトリー 容量 ======================================================================================================================== インストール中: mysql-community-client x86_64 8.0.18-1.el7 mysql80-community 38 M mysql-community-libs x86_64 8.0.18-1.el7 mysql80-community 3.7 M mariadb-libs.x86_64 1:5.5.64-1.amzn2 を入れ替えます mysql-community-libs-compat x86_64 8.0.18-1.el7 mysql80-community 1.3 M mariadb-libs.x86_64 1:5.5.64-1.amzn2 を入れ替えます 依存性関連でのインストールをします: mysql-community-common x86_64 8.0.18-1.el7 mysql80-community 597 k↓
インストールしたmysqlのバージョンを確認。$ mysql --version mysql Ver 8.0.18 for Linux on x86_64 (MySQL Community Server - GPL)↓
本件目的のRDS接続を試みる。接続できるようになった。$ mysql -h 'エンドポイント' -P 'ポート番号' -u 'ユーザー名' -p Enter password: 'パスワード' : : : mysql>参考
- 投稿日:2019-12-02T16:06:33+09:00
Laravel+Vue.js+MySQLで入力内容の途中保存機能を実装してみた
グレンジ Advent Calendar 2019 4日目担当の soyo と申します。
グレンジでクライアントエンジニアをしております。
とはいえ、今年の記事もクライアントとはまったく関係ありません。普段Googleフォームなどでアンケートを回答する際に、
「あれ、途中で保存することができないの?」って自分はたまに思います。ユーザーが一項目ずつ入力したらサーバーに送信してデータベースに記録するから、
ページに再度アクセスしたら記録されている情報を自動的に反映するまで、
PHPを使って簡単に実装してみました。目標
「ラジオボタンの選択内容」と「テキストの入力内容」を途中保存できるようにする
開発環境
- macOS 10.14.6
- PHP 7.3.8
- Laravel 6.6.0
- MySQL 8.0.18
フロントエンド
Vue.jsで入力内容の操作
今回の戦場はLaravelプロジェクトのwelcome画面にします。
まずはそこにVue.jsを導入して、ラジオボタン3つとテキストボックス1つを置きます。resources/views/welcome.blade.php... <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> ... <div id="app"> ラジオボタン<br/> <input type="radio" value="1" v-model="radio">選択肢1<br/> <input type="radio" value="2" v-model="radio">選択肢2<br/> <input type="radio" value="3" v-model="radio">選択肢3<br/> <br/> テキスト入力<br/> <input type="text" v-model="text" placeholder="内容を入力"> <br/> </div> ...public/js/main.jsconst app = new Vue({ el: '#app', data: { radio: '2', text: 'あいうえお' }, });これで
radio
とtext
でラジオボタンとテキストボックスを操作することができます。
UUIDの作成と保存
javascriptで適当なUUIDを生成する方法がありまして、
生成したUUIDをJavaScript Cookieでローカルに保存するようにします。resources/views/welcome.blade.php... <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script> ...public/js/main.jsconst app = new Vue({ el: '#app', data: { radio: '', text: '', uuid: '' }, methods: { initUUID: function() { if (Cookies.get('uuid') !== undefined) { this.uuid = Cookies.get('uuid'); return; } var d = new Date().getTime(); var d2 = (performance && performance.now && (performance.now() * 1000)) || 0; this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16; if (d > 0){ r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else { r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); // とりあえず期限を10年にする Cookies.set('uuid', this.uuid, { expires: 3650 }); } } }); app.initUUID();これで画面を開く度にcookieからuuidを取得し、存在しない場合は生成できるようになりました。
サーバーとの通信
サーバーとの通信はaxiosで行います。
resources/views/welcome.blade.php... <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script> ...public/js/main.jsconst app = new Vue({ ... methods: { saveData: function(key, value) { let postData = { 'user_id': this.uuid, 'key': key, 'value': value }; axios.post("/saveData", postData).then(response => { // 成功 }).catch(error => { // 失敗 }); }, loadData: function () { let postData = { 'user_id': this.uuid }; axios.post("/loadData", postData).then(response => { // 成功 }).catch(error => { // 失敗 }); } } });送信する内容についてですが、
文字を入力する度に送信してしまうとサーバーに負荷をかける可能性がありますので、
今回は連続する入力を無視してくれるLodashのdebounceで制御します。resources/views/welcome.blade.php... <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> ... <div id="app"> ラジオボタン<br/> <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/> <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/> <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/> <br/> テキスト入力<br/> <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力"> <br/> </div> ...public/js/main.jsconst app = new Vue({ el: '#app', data: { ... isTextTyping: false, isRadioSelecting: false, ... }, watch: { radio: _.debounce(function() { this.isRadioSelecting = false; }, 1000), text: _.debounce(function() { this.isTextTyping = false; }, 2000), isRadioSelecting: function(selecting) { if (selecting) { return; } this.saveData('radio', this.radio); }, isTextTyping: function(typing) { if (typing) { return; } this.saveData('text', this.text); }, }, ... });これでラジオボタンは選択停止後1秒、テキストボックスは入力停止後2秒からサーバーにデータを送るようになりました。
最後に、ステータスをわかるためにvue2-notifyを使ってプッシュ通知を表示させます。
resources/views/welcome.blade.php... <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> ...使い方の例
this.$notify.info({ title: '受信', message: '内容読み取り完了' });これで、フロントエンドの方は必要な機能を揃えました。
完成したコードはこの記事の最後にまとめております。サーバーサイド
データベース構造
テストのため、すごくシンプルなテーブルを作ります。
+------------+ | database() | +------------+ | vue_test | +------------+ +------------+ | TABLE_NAME | +------------+ | user_input | +------------+ +-------------+-----------+ | COLUMN_NAME | DATA_TYPE | +-------------+-----------+ | id | int | | user_id | varchar | | radio | int | | text | varchar | +-------------+-----------+リクエストデータ処理クラス
ユーザー入力内容をデータベースに書き込む・読み取り処理を行います。
app/Http/Controllers/UserInputController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class UserInputController extends Controller { public function saveData(Request $request) { DB::table('user_input')->updateOrInsert( [ 'user_id' => $request->input('user_id') ], [ $request->input('key') => $request->input('value') ] ); } public function loadData(Request $request) { $user_id = $request->input('user_id'); $data = [ 'result' => DB::table('user_input')->where('user_id', $user_id)->first() ]; return $data; } }ルーティング
routes/web.php... Route::post('/saveData', 'UserInputController@saveData'); Route::post('/loadData', 'UserInputController@loadData'); ...コードまとめ
resources/views/welcome.blade.php<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> </head> <body> <div id="app"> ラジオボタン<br/> <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/> <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/> <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/> <br/> テキスト入力<br/> <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力"> <br/> </div> </body> <script src="{{ asset('/js/main.js') }}"></script> </html>public/js/main.jsconst app = new Vue({ el: '#app', data: { radio: '', text: '', isTextTyping: false, isRadioSelecting: false, uuid: '' }, watch: { radio: _.debounce(function() { this.isRadioSelecting = false; }, 1000), text: _.debounce(function() { this.isTextTyping = false; }, 2000), isRadioSelecting: function(selecting) { if (selecting) { return; } this.saveData('radio', this.radio, 'ラジオボタン'); }, isTextTyping: function(typing) { if (typing) { return; } this.saveData('text', this.text, 'テキスト入力'); }, }, methods: { initUUID: function() { if (Cookies.get('uuid') !== undefined) { this.uuid = Cookies.get('uuid'); return; } var d = new Date().getTime(); var d2 = (performance && performance.now && (performance.now() * 1000)) || 0; this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16; if (d > 0){ r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else { r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); Cookies.set('uuid', this.uuid, { expires: 3650 }); }, saveData: function(key, value, description) { let postData = { 'user_id': this.uuid, 'key': key, 'value': value }; axios.post("/saveData", postData).then(response => { this.$notify.info({ title: '送信', message: '内容保存済み:' + description }); }).catch(error => { this.$notify.error({ title: '送信', message: '送信に失敗しました' }) }); }, loadData: function () { let postData = { 'user_id': this.uuid }; axios.post("/loadData", postData).then(response => { let data = response.data['result']; if (data == null) { this.$notify.info({ title: '受信', message: '新規ユーザー' }); return; } this.radio = data['radio']; this.text = data['text']; this.$notify.info({ title: '受信', message: '内容読み取り完了' }); }).catch(error => { this.$notify.error({ title: '受信', message: '受信に失敗しました' }) }); } } }); app.initUUID(); app.loadData();app/Http/Controllers/UserInputController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class UserInputController extends Controller { public function saveData(Request $request) { DB::table('user_input')->updateOrInsert( [ 'user_id' => $request->input('user_id') ], [ $request->input('key') => $request->input('value') ] ); } public function loadData(Request $request) { $user_id = $request->input('user_id'); $data = [ 'result' => DB::table('user_input')->where('user_id', $user_id)->first() ]; return $data; } }routes/web.php<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Route::post('/saveData', 'UserInputController@saveData'); Route::post('/loadData', 'UserInputController@loadData');最後に
Vue.jsが使いやすくて、サードパーティのライブラリもたくさんあって、
導入と実装がかなり楽でした(cocos2d-xとunityと比べるとねw)また、項目を増やす度にテーブルにカラムを追加するのはさすがに面倒ですね。
その場合はテーブルのスキーマをユーザーID、項目ID、内容にして、
select文でユーザーIDと項目IDで検索して、その結果を処理して反映すればいいと思います。ありがとうございました。
- 投稿日:2019-12-02T11:15:02+09:00
docker-composeでMySQLを起動してみた
docker-composeでMySQLを起動してみた
開発環境
- Windows10
- IntelliJIDE
- Docker for Windows
- MySQL8.0
作業内容
- docker-composeを利用して、MySQLイメージをコンテナで起動
docker-composeでMySQL構築
- docker-composeのバ‐ジョン確認
>docker-compose --version docker-compose version 1.24.1, build 4667896b
- ディレクトリの作成
prototype-docker/ ├ docker/ | └ mysql/ | ├ conf.d/ | | └ my.cnf | ├ initdb.d/ | | ├ schema.sql | | └ testdata.sql | └ Dockerfile └ docker-compose.yml
- Dockerfileの作成
Dockerfileにイメージのビルド内容を記述します。
FROM mysql:8.0 # 指定の場所にログを記録するディレクトリを作る RUN mkdir /var/log/mysql # 指定の場所にログを記録するファイルを作る RUN touch /var/log/mysql/mysqld.log
- docker-compose.ymlの作成
docker-compose.ymlにMYSQLの設定を記述します。
version: '3.3' services: db: build: ./docker/mysql image: mysql:8.0 restart: always environment: MYSQL_DATABASE: prototype MYSQL_USER: user MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password TZ: 'Asia/Tokyo' ports: - "3306:3306" volumes: - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d - ./docker/mysql/conf.d:/etc/mysql/conf.d
- my.confの作成
my.confに、独自のMySQLの設定を記述します。
[mysqld] # mysqlサーバー側が使用する文字コード character-set-server=utf8mb4 # テーブルにTimeStamp型のカラムをもつ場合、推奨 explicit-defaults-for-timestamp=1 # 実行したクエリの全ての履歴が記録される(defaultではOFF) general-log=1 # ログの出力先 general-log-file=/var/log/mysql/mysqld.log [client] # mysqlのクライアント側が使用する文字コード default-character-set=utf8mb4
- schema.sqlの作成
初期化するテーブル定義のDDLを記述します。
create TABLE IF NOT EXISTS `prototype`.`users` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT 'ユーザーID' , `mail` VARCHAR (256) NOT NULL COMMENT 'メールアドレス' , `gender` SMALLINT (1) NOT NULL COMMENT '性別' , `password` VARCHAR (256) NOT NULL COMMENT 'パスワード' , `birthdate` DATE NOT NULL COMMENT '生年月日' , `create_user_id` INT NULL COMMENT '作成者ID' , `create_timestamp` TIMESTAMP NULL COMMENT '作成日時' , PRIMARY KEY (`id`) , UNIQUE INDEX `mail_UNIQUE` (`mail`) ) ENGINE = Innodb , DEFAULT character set utf8 , COMMENT = 'ユーザー' ;
- testdata.sqlの作成
初期化したテーブルに投入するデータのDMLを記述します。
INSERT INTO users(mail, gender, password, birthdate, create_user_id, create_timestamp) VALUES ('test@gmail.com', 1, '暗号化したパスワード', '1991/01/01', 0, current_timestamp);
- docker-composeコマンドの実行
ディレクトリ構成と各種ファイルの作成が完了したら、docker-composeコマンドを実行します。
> docker-compose up -d #コンテナの起動 Creating prototype-docker_db_1 ... done >docker-compose ps #存在するコンテナの一覧とその状態を表示 Name Command State Ports ----------------------------------------------------------------------------------------------- prototype-docker_db_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp, 33060/tcp
- 初期化クエリ実行確認
schema.sqlとtestdata.sqlの実行結果を確認します。
>docker exec -it prototype-docker_db_1 bash root@aabd0f319f85:/# mysql -u user -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 24 Server version: 8.0.18 MySQL Community Server - GPL Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> show tables from prototype; +---------------------+ | Tables_in_prototype | +---------------------+ | users | | users_history | +---------------------+ 2 rows in set (0.00 sec)ソースコード
- 投稿日:2019-12-02T09:49:57+09:00
CakePHP2でgroup byして最新レコード取得
CakePHP2のfind()では特定IDでGROUP BYした上でそれぞれの最新レコード取得するのが難しかったので後の為にメモ
test_page_historyCREATE TABLE `test_page_history` ( `id` int(11) NOT NULL AUTO_INCREMENT, `page_id` varchar(50) NOT NULL, `update_date` datetime NOT NULL, `updater_name` varchar(50) NOT NULL, `reason` varchar(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `test_UN` (`page_id`,`update_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4①のようなテストデータから②を取得したい
① 元データ
"id" "page_id" "update_date" "updater_name" "reason" 1 A-1 "2019-12-01 00:00:00.0" alice ページ新規追加 2 A-2 "2019-12-02 00:00:00.0" bob ページ新規追加 3 A-1 "2019-12-05 00:00:00.0" charlie 機能追加 4 A-3 "2019-12-05 00:00:00.0" alice 新規追加 5 A-1 "2019-12-06 00:00:00.0" delta 不要の為ページ削除 6 A-3 "2019-12-07 00:00:00.0" charlie デザイン修正 ② page_idでまとめた最新レコード
"id" "page_id" "update_date" "updater_name" "reason" 5 A-1 "2019-12-06 00:00:00.0" delta 不要の為ページ削除 2 A-2 "2019-12-02 00:00:00.0" bob ページ新規追加 6 A-3 "2019-12-07 00:00:00.0" charlie デザイン修正
- 投稿日:2019-12-02T09:49:57+09:00
【CakePHP2】任意のカラムでGROUP BYした最新レコードの取得
内容
CakePHP2のfind()では特定のカラムでGROUP BYした上でそれぞれの最新レコード取得するのが難しかったので後の為にメモ
やりたいこと
下記のようなテーブルでpage_idをGROUP BYしてそのページごとの最新レコードが欲しい
あと、できればpaginatorされてほしいtest_page_history.sqlCREATE TABLE `test_page_history` ( `id` int(11) NOT NULL AUTO_INCREMENT, `page_id` varchar(50) NOT NULL, `update_date` datetime NOT NULL, `updater_name` varchar(50) NOT NULL, `reason` varchar(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `test_UN` (`page_id`,`update_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4① 元データ
"id" "page_id" "update_date" "updater_name" "reason" 1 A-1 "2019-12-01 00:00:00.0" alice ページ新規追加 2 A-2 "2019-12-02 00:00:00.0" bob ページ新規追加 3 A-1 "2019-12-05 00:00:00.0" charlie 機能追加 4 A-3 "2019-12-05 00:00:00.0" alice 新規追加 5 A-1 "2019-12-06 00:00:00.0" delta 不要の為ページ削除 6 A-3 "2019-12-07 00:00:00.0" charlie デザイン修正 ② 取得したいテータ(page_idでまとめた最新レコード)
"id" "page_id" "update_date" "updater_name" "reason" 5 A-1 "2019-12-06 00:00:00.0" delta 不要の為ページ削除 2 A-2 "2019-12-02 00:00:00.0" bob ページ新規追加 6 A-3 "2019-12-07 00:00:00.0" charlie デザイン修正 やったこと
Modelにカスタムfinderを追加実装し
conditionsで力業を使ったTestPageHistory.php<?php App::uses('AppModel', 'Model'); class TestPageHistory extends AppModel { public $useTable = 'test_page_history'; public $primaryKey = 'id'; public $useDbConfig = 'admin'; // 自作するカスタムfinderを使用可能に設定 public $findMethods = [ 'newest' => true, ]; /** * newest実装 * * @param string $state * @param array $query * @param array $results */ protected function _findNewest($state, $query, $results = []) { if ($state === 'before') { $query['conditions']["{$this->alias}.update_date = (SELECT MAX(b.update_date) FROM test_page_history AS b WHERE TestPageHistory.page_id = b.page_id)"] = [true]; $query['order']["{$this->alias}.page_id"] = ['asc']; return $query; } return $results; } }Controllerはカスタムfinderを使うのみ
TestController.php<?php App::uses('AppController', 'Controller'); App::uses('TestPageHistory', 'Model'); class PointsListController extends AppController { public $uses = [ 'TestPageHistory', ]; /** * 一覧 * * @return void */ public function index(): void { // カスタムfinderである「'newest'」でデータ取得 $test = $this->TestPageHistory->find('newest'); var_dump($test); }↓
取得結果
page_idごとの最新レコードが取得できている?
array(3) { [0]=> array(1) { ["TestPageHistory"]=> array(5) { ["id"]=> string(1) "5" ["page_id"]=> string(3) "A-1" ["update_date"]=> string(19) "2019-12-06 00:00:00" ["updater_name"]=> string(5) "delta" ["reason"]=> string(27) "不要の為ページ削除" } } [1]=> array(1) { ["TestPageHistory"]=> array(5) { ["id"]=> string(1) "2" ["page_id"]=> string(3) "A-2" ["update_date"]=> string(19) "2019-12-02 00:00:00" ["updater_name"]=> string(3) "bob" ["reason"]=> string(21) "ページ新規追加" } } [2]=> array(1) { ["TestPageHistory"]=> array(5) { ["id"]=> string(1) "6" ["page_id"]=> string(3) "A-3" ["update_date"]=> string(19) "2019-12-07 00:00:00" ["updater_name"]=> string(7) "charlie" ["reason"]=> string(18) "デザイン修正" } } }paginatorにもつっこめた
TestController.php$this->paginate = [ 'conditions' => $conditions, 'limit' => $limit, 'findType' => 'newest', ]; $this->paginate();
- 投稿日:2019-12-02T09:49:57+09:00
CakePHP2にて任意カラムでGROUP BYした最新レコードの取得
内容
CakePHP2のfind()では特定のカラムでGROUP BYした上でそれぞれの最新レコード取得するのが難しかったので後の為にメモ
やりたいこと
下記のようなテーブルでpage_idをGROUP BYしてそのページごとの最新レコードが欲しい
あと、できればpaginatorされてほしいtest_page_history.sqlCREATE TABLE `test_page_history` ( `id` int(11) NOT NULL AUTO_INCREMENT, `page_id` varchar(50) NOT NULL, `update_date` datetime NOT NULL, `updater_name` varchar(50) NOT NULL, `reason` varchar(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `test_UN` (`page_id`,`update_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4① 元データ
"id" "page_id" "update_date" "updater_name" "reason" 1 A-1 "2019-12-01 00:00:00.0" alice ページ新規追加 2 A-2 "2019-12-02 00:00:00.0" bob ページ新規追加 3 A-1 "2019-12-05 00:00:00.0" charlie 機能追加 4 A-3 "2019-12-05 00:00:00.0" alice 新規追加 5 A-1 "2019-12-06 00:00:00.0" delta 不要の為ページ削除 6 A-3 "2019-12-07 00:00:00.0" charlie デザイン修正 ② 取得したいテータ(page_idでまとめた最新レコード)
"id" "page_id" "update_date" "updater_name" "reason" 5 A-1 "2019-12-06 00:00:00.0" delta 不要の為ページ削除 2 A-2 "2019-12-02 00:00:00.0" bob ページ新規追加 6 A-3 "2019-12-07 00:00:00.0" charlie デザイン修正 やったこと
Modelにカスタムfinderを追加実装し
conditionsで力業を使ったTestPageHistory.php<?php App::uses('AppModel', 'Model'); class TestPageHistory extends AppModel { public $useTable = 'test_page_history'; public $primaryKey = 'id'; public $useDbConfig = 'admin'; // 自作するカスタムfinderを使用可能に設定 public $findMethods = [ 'newest' => true, ]; /** * newest実装 * * @param string $state * @param array $query * @param array $results */ protected function _findNewest($state, $query, $results = []) { if ($state === 'before') { $query['conditions']["{$this->alias}.update_date = (SELECT MAX(b.update_date) FROM test_page_history AS b WHERE TestPageHistory.page_id = b.page_id)"] = [true]; $query['order']["{$this->alias}.page_id"] = ['asc']; return $query; } return $results; } }Controllerはカスタムfinderを使うのみ
TestController.php<?php App::uses('AppController', 'Controller'); App::uses('TestPageHistory', 'Model'); class PointsListController extends AppController { public $uses = [ 'TestPageHistory', ]; /** * 一覧 * * @return void */ public function index(): void { // カスタムfinderである「'newest'」でデータ取得 $test = $this->TestPageHistory->find('newest'); var_dump($test); }↓
取得結果
page_idごとの最新レコードが取得できている?
array(3) { [0]=> array(1) { ["TestPageHistory"]=> array(5) { ["id"]=> string(1) "5" ["page_id"]=> string(3) "A-1" ["update_date"]=> string(19) "2019-12-06 00:00:00" ["updater_name"]=> string(5) "delta" ["reason"]=> string(27) "不要の為ページ削除" } } [1]=> array(1) { ["TestPageHistory"]=> array(5) { ["id"]=> string(1) "2" ["page_id"]=> string(3) "A-2" ["update_date"]=> string(19) "2019-12-02 00:00:00" ["updater_name"]=> string(3) "bob" ["reason"]=> string(21) "ページ新規追加" } } [2]=> array(1) { ["TestPageHistory"]=> array(5) { ["id"]=> string(1) "6" ["page_id"]=> string(3) "A-3" ["update_date"]=> string(19) "2019-12-07 00:00:00" ["updater_name"]=> string(7) "charlie" ["reason"]=> string(18) "デザイン修正" } } }paginatorにもつっこめた
TestController.php$this->paginate = [ 'conditions' => $conditions, 'limit' => $limit, 'findType' => 'newest', ]; $this->paginate();
- 投稿日:2019-12-02T01:53:11+09:00
Laradockを用いてDocker/Apache/PHP7.2/MySQL/Laravelの開発環境を構築する
Laradockクローン
ルートディレクトリにて、下記を実行。
$ git clone https://github.com/LaraDock/laradock.git
.envをenv-exampleからコピーして作成。
$ cd laradock
$ cp env-example .env
プロジェクト作成まずは、ワークスペースを起動。
$ cd laradock
$ docker-compose up -d workspace
ワークスペースに入る。
$ docker-compose exec workspace bash
Laravelのプロジェクトを作成composer create-project laravel/laravel web
dockerを一旦終了
exit
$ docker-compose down
laradock/.envのpathを作成したプロジェクトに変更。Point to the path of your applications code on your host
APP_CODE_PATH_HOST=../new_project
apache2の設定変更
ハマったところ。
apache2を使用するので、laradock/apache2/sites/default.apache.confを変更。
ServerName localhost
DocumentRoot /var/www/public/
Options Indexes FollowSymLinks各バージョンを指定(.env)
PHP_VERSION=7.2
MYSQL_VERSION=latest
(mysql/Dockerfile)
ARG MYSQL_VERSION=5.7dockerにてコンテナを起動。
$ docker-compose up -d mysql apache2 workspace
localhostにアクセス。docker-compose stop
既存のLaravelプロジェクトを配置する場合
webに展開
docker-compose exec --user=laradock workspace bash # workspaceへ入る
composer install
laradock@hoge:/var/www$ exit # workspaceから抜ける
$ docker-compose restart # コンテナ再起動
http://localhost/ にアクセスLaravel のプロジェクトを Homestead 環境で 起動させました。
http://localhost:8000/ にアクセスするとエラーがでました。RuntimeException がでる
RuntimeException
No application encryption key has been specified.
encryption key がないとあります。key を生成する
php artisan key:generate
Application key [base64:Wdhku6YSePiOh0XjqauthSaeOhzwRKxasFjbuuHXz0w=] set successfully.
Application key が生成されました。再度アクセス
http://localhost:8000/ にアクセスすると Laravel の初期画面が表示されました