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

本番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を入れる
    • これは今でもトラウマなのでチェックします

ひとこと

ポストモーテムを書くのも読むのもトラブルシューティング力を高めるきっかけになるかと思います!
みなさんももっと恥を忍んでさらけ出しましょう!

参考リンク

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

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

これにより、標準の設定が上書きされます。サーバーを再起動しましょう。

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

MySQL 8.0.18でパスワードリセットしてもrootユーザにログインできなくなった時の解決方法

目的

  • 何をしてもMySQLのrootユーザにログインできなくなった時の解決方法を書く

結論

  • MySQLをアンインストール→インストールしてrootユーザのパスワードを再登録した。

実施概要

  1. MySQLのファイル削除とアンインストールとインストール
  2. パスワードの再設定とログイン確認

実施詳細

  1. MySQLのファイル削除とアンインストールとインストール

    1. 下記コマンドを実行してMySQLのファイルを削除する。(DB内のデータも消すことになるので注意!何もせず下記コマンドを実行するとデータベースに保存されている内容が全て吹き飛ぶ)

      $ sudo rm -rf /usr/local/var/mysql
      
    2. 下記コマンドを実行してMySQLをアンインストールする。

      $ brew uninstall mysql
      
    3. 下記コマンドを実行してMySQLをインストールする。

      $ brew install mysql
      
  2. パスワードの再設定とログイン確認

    1. 下記コマンドを実行してMySQLを起動する。

      $ mysql.server restart
      
    2. 下記コマンドを実行してMySQLに入る。

      $ mysql -u root -p 
      
    3. 下記コマンドを実行してパスワードを設定する。

      mysql>USE mysql;
      mysql>ALTER USER 'root'@'localhost' identified BY '任意のパスワード';
      
    4. 下記コマンドを実行してMySQLを抜ける。

      mysql>exit
      
    5. 下記コマンドを実行後、パスワードの入力を求められるため入力してログインできるか確認する。

      $ mysql -u root -p
      
    6. 下記のようになればOKである。

      mysql>
      
    7. あとはcreateしてmigrateすればテーブル構造は元どおりになる。

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

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 cleardb

Herokuでは、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.js
connection.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"
  },
--- 省略 ---

デプロイ

最後にデプロイをして終了です。

実際に使ってみましょう。

スクリーンショット 2019-12-02 18.28.30.png

実際にPOSTを送ってみた際のスクショです。
いい感じですね。

終わり

次回は、このAPIを使用してTODOアプリを開発していきます。
何か間違いがある際はおしらせください!

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

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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MySQLで外部キー制約の一覧を取得する方法

mysql
select * from `information_schema`.table_constraints where constraint_type="FOREIGN KEY";

テーブルを削除しようとして外部キー制約で困った場合にどうぞ

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

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-communiry-downloads.jpg

上記ページから移動した先のページに記載してあるダウンロードリンクをコピペして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>

参考

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

Laravel+Vue.js+MySQLで入力内容の途中保存機能を実装してみた

グレンジ Advent Calendar 2019 4日目担当の soyo と申します。
グレンジでクライアントエンジニアをしております。
とはいえ、今年の記事もクライアントとはまったく関係ありません。

普段Googleフォームなどでアンケートを回答する際に、
「あれ、途中で保存することができないの?」って自分はたまに思います。

ユーザーが一項目ずつ入力したらサーバーに送信してデータベースに記録するから、
ページに再度アクセスしたら記録されている情報を自動的に反映するまで、
PHPを使って簡単に実装してみました。

目標

「ラジオボタンの選択内容」と「テキストの入力内容」を途中保存できるようにする
2.png

開発環境

  • 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.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '2',
        text: 'あいうえお'
    },
});

これでradiotextでラジオボタンとテキストボックスを操作することができます。
1.png

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.js
const 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.js
const 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 => {
                // 失敗
            });
        }
    }
});

送信する内容についてですが、
文字を入力する度に送信してしまうとサーバーに負荷をかける可能性がありますので、
今回は連続する入力を無視してくれるLodashdebounceで制御します。

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.js
const 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.js
const 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で検索して、その結果を処理して反映すればいいと思います。

ありがとうございました。

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

docker-composeでMySQLを起動してみた

docker-composeでMySQLを起動してみた

開発環境

  • Windows10
  • IntelliJIDE
  • Docker for Windows
  • MySQL8.0

作業内容

  • docker-composeを利用して、MySQLイメージをコンテナで起動

docker-composeでMySQL構築

  1. docker-composeのバ‐ジョン確認
>docker-compose --version
docker-compose version 1.24.1, build 4667896b
  1. ディレクトリの作成
prototype-docker/
             ├ docker/
             |       └ mysql/
             |              ├ conf.d/
             |              |       └ my.cnf
             |              ├ initdb.d/
             |              |         ├ schema.sql
             |              |         └ testdata.sql
             |              └ Dockerfile
             └ docker-compose.yml 
  1. Dockerfileの作成

Dockerfileにイメージのビルド内容を記述します。

FROM mysql:8.0
# 指定の場所にログを記録するディレクトリを作る
RUN mkdir /var/log/mysql
# 指定の場所にログを記録するファイルを作る
RUN touch /var/log/mysql/mysqld.log
  1. 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
  1. 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
  1. 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 = 'ユーザー'
;
  1. 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);
  1. 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
  1. 初期化クエリ実行確認

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)

ソースコード

ref: https://github.com/forests-k/prototype-docker

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

CakePHP2でgroup byして最新レコード取得

CakePHP2のfind()では特定IDでGROUP BYした上でそれぞれの最新レコード取得するのが難しかったので後の為にメモ

test_page_history
CREATE 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 デザイン修正
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【CakePHP2】任意のカラムでGROUP BYした最新レコードの取得

内容

CakePHP2のfind()では特定のカラムでGROUP BYした上でそれぞれの最新レコード取得するのが難しかったので後の為にメモ

やりたいこと

下記のようなテーブルでpage_idをGROUP BYしてそのページごとの最新レコードが欲しい
あと、できればpaginatorされてほしい

test_page_history.sql
CREATE 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();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CakePHP2にて任意カラムでGROUP BYした最新レコードの取得

内容

CakePHP2のfind()では特定のカラムでGROUP BYした上でそれぞれの最新レコード取得するのが難しかったので後の為にメモ

やりたいこと

下記のようなテーブルでpage_idをGROUP BYしてそのページごとの最新レコードが欲しい
あと、できればpaginatorされてほしい

test_page_history.sql
CREATE 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();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.7

dockerにてコンテナを起動。
$ 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 の初期画面が表示されました

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