狛ログ

ラベル MySQL の投稿を表示しています。 すべての投稿を表示
ラベル MySQL の投稿を表示しています。 すべての投稿を表示

2020年11月25日水曜日

WindowsでDockerを利用してMySQLサーバーを立てる場合の注意点。

11月 25, 2020
オフィス狛 技術部のHammarです。

最近はnode.jsを触る機会がまた多くなってきたのですが、あるプロジェクトでAPIはnode.js、DBはMysqlを使うことになったので、Dockerでそれらのローカル環境を作り開発することにしました。
そこでWindowsとDockerの絡みでハマりポイントがあったので、ご紹介したいと思います。

※今回はDockerがWindows10環境にインストールされている前提で以下進めていきます。

まずDockerの起動にはdocker-compose.ymlを利用していきます。
そのdocker-compose.ymlには今回開発につかうAPIとDBの設定を下記のように記載します。
※今回DBの記述がメインとなるので、API側の記述は割愛します。
version: '3'
services:
  api:
	build:・・・
	・・・
  db:
    image: mysql:8.0
    command: mysqld --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
      TZ: 'Asia/Tokyo'
    ports:
      - "3306:3306"
    volumes:
      - './docker/dev/mysql/data:/var/lib/mysql'
      - './docker/dev/mysql/my.cnf:/etc/mysql/conf.d/my.cnf'
      - './docker/dev/mysql/sql:/docker-entrypoint-initdb.d'

上記のように書いて、あとは通常通り上記ymlを使ってDocker起動するだけです。
しかしながら、上記方法でMacでは上手くいくのですが、Windowsではなぜかうまくいきません。

これはWindowsはMacとは違い、WindowsはdockerをVirtualBox経由で起動させているためのようです。
こちら参考にさせていただきました。
https://qiita.com/waterada/items/1dbf6a977611e0e8f5c8

※ちなみにWindowsは上記が原因で、作りたい環境内容によっては他の問題も発生するようで、実際自分も別環境作成時にまた問題があったのですが、またそのあたり別の投稿で記載しようと思います。

ということでいろいろと調べた結果、mysqlの設定ファイルであるmy.cnfファイルを別途作成し、それをdocker起動時にマウントさせることによって本事象を解消できるということのようでした。

1.docker-compose.ymlを修正する

まずはdocker-compose.ymlを下記のように書き換えます。
version: '3'
services:
  api:
	build:・・・
	・・・
  db:
    image: mysql:8.0
    command: mysqld --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
      TZ: 'Asia/Tokyo'
    ports:
      - "3306:3306"
    volumes:
      - './docker/dev/mysql/sql:/docker-entrypoint-initdb.d'

2.my.cnfファイルを作成する

もともとDBの文字コードの設定もymlファイルのcommand部分に記載していたのですが、このあたりをmy.cnfに別途下記のように作成します。
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[client]
default-character-set=utf8mb4

3.Dockerfileを作成する

最後に上記のmy.cnfファイルをマウントさせるために、下記のようにDockerfileを作成し、そこでADDします。
またファイルの権限もデフォルトだと777となってしまい、mysqlは権限777のcnfファイルは読み込まないということなので、ADDしたあとに権限も変更するように記述します。
FROM mysql:8.0

ADD ./docker/dev/mysql/my.cnf /etc/mysql/conf.d/my.cnf

RUN chmod 644 /etc/mysql/conf.d/my.cnf

上記3つが整った状態で、ymlを使ってDocker起動すると、なんとかうまく起動されました。
上記手順書いてみると、なるほどなーと感じるのですが、これを全く知らないところから調べていったので、かなりハマって環境構築だけで結構時間がかかってしまいました。。。
そもそもやっぱりVirtualBox経由でDockerを利用することがいろいろな弊害を生んでいるようなので、このあたりWindowsユーザーは不利だなーと感じたのでした。

2019年3月28日木曜日

MySQLでAUTO INCREMENTの値を取得したい。

3月 28, 2019

オフィス狛 技術部のJoeです。

INSERT後にAUTO INCREMENTで自動採番された値を使用したいことがあるかと思いますが、
MySQLでは前回INSERTのAUTO INCREMENT値を「LAST_INSERT_ID()」関数で取得できます。

テーブルを作成して、1件INSERTすると「1」が取れました。
mysql> CREATE TABLE test (
    -> id   INT         NOT NULL PRIMARY KEY AUTO_INCREMENT,
    -> name VARCHAR(10) NOT NULL );
mysql> INSERT INTO test ( name ) VALUES ( '111' );
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

AUTO INCREMENTは一意であることを保証してくれますので、各セッション内で最後にINSERTした値が取れました。
【セッションA】INSERT INTO test ( name ) VALUES ( '111' );
【セッションB】INSERT INTO test ( name ) VALUES ( '222' );
【セッションB】SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                2 |
+------------------+
【セッションA】SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

と、便利な関数ですが、想定する値が取れないパターンも併せてをご紹介します。

●AUTO INCREMENTのカラムに値を指定してINSERTした場合
値を指定してしまうと取得できません。同一セッションで前回自動採番された値が取れるようです。
mysql> INSERT INTO test ( name ) VALUES ( '111' ); -- 値を指定しない(自動採番)
mysql> INSERT INTO test ( id, name ) VALUES ( 10, '222' ); -- 値を指定する
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

●単一のINSERT文で複数データをINSERTした場合
3件INSERTしたので「3」が欲しいのですが、「1」が取れました。
mysql> INSERT INTO test ( name ) VALUES ( '111' ), ( '222' ), ( '333' );
mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 |
+------------------+

●別セッションの場合
「当然!」と言われそうですが、別セッションでは取得できません。
下記のようなDB接続から開始するAUTO INCREMENT値の取得処理みたいなものを作ってしまうと、想定した値が取れませんでした。
(MySQLへの接続は「MySql.Data.MySqlClient」を使っています)

[ C#でMySQL接続してAUTO INCREMENT値を取得(ダメな例) ]
string conString= "Database=test;Data Source=localhost;User Id=test;Password=test";
public void Index()
{
    using (MySqlConnection con = new MySqlConnection(conString))
    {
        // DB接続→INSERT
        con.Open();
        MySqlCommand cmd = con.CreateCommand();
        MySqlTransaction tran;
        tran = con.BeginTransaction();
        cmd.Connection = con;
        cmd.Transaction = tran;
        try
        {
            cmd.CommandText = "INSERT INTO test (name) VALUES ('1111')";
            cmd.ExecuteNonQuery();
            tran.Commit();
            ShowAutoIncrementId();  // AutoIncrementの値を表示
        }
        catch (Exception)
        {
            tran.Rollback();
        }
    }
}

public void ShowAutoIncrementId()
{
    using (MySqlConnection con = new MySqlConnection(conString))
    {
        // DB接続→AutoIncrementIdを取得して表示する
        con.Open();
        MySqlCommand cmd = con.CreateCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT LAST_INSERT_ID()";
        object id = cmd.ExecuteScalar();
        System.Diagnostics.Debug.WriteLine("***** ID : " + id);
        return;
    }
}

【1回目の実行】
INSERTとSELECTが別セッション(428と429)になってしまったので取得できませんでした。
■結果
***** ID : 0
■MySQLログ
2019-03-06T07:39:31.579862Z 428 Query INSERT INTO test (name) VALUES ('111')
2019-03-06T07:39:31.582475Z 428 Query COMMIT
2019-03-06T07:39:31.660854Z 429 Connect test@localhost on test using SSL/TLS
2019-03-06T07:39:31.668533Z 429 Init DB test
2019-03-06T07:39:31.668751Z 429 Query SELECT LAST_INSERT_ID()

【2回目の実行】
なぜか「1」が取れました!?
コネクションプールで、1回目のINSERTと2回目のSELECTのセッションIDが同じ(428)になると、前回INSERTのIDが取得できてしまうようです。
■結果
***** ID : 1
■MySQLログ
2019-03-06T07:39:40.390591Z 429 Query INSERT INTO test (name) VALUES ('111')
2019-03-06T07:39:40.393070Z 429 Query COMMIT
2019-03-06T07:39:40.469436Z 428 Init DB test
2019-03-06T07:39:40.469689Z 428 Query SELECT LAST_INSERT_ID()


AUTO INCREMENTの値は、主キーや外部キーなどで使用することがあるかと思いますので、
「LAST_INSERT_ID()」関数を使用される際はご注意ください。

2017年1月21日土曜日

MySQL では、'a' = 0 が True になる。

1月 21, 2017
オフィス狛 技術部です。

MySQL において、文字列と数値の比較は思わぬ結果になってしまう事があるので注意です。

まず、題名にもある通り、

「SELECT 0 = 'a';」はTrueになります。

・なぜこんな事が起きてしまうのか?

MySQLでは、型が違う値同士を比較すると、暗黙的な変換が発生します。
文字列の「1」は、数値の「1」になります。
しかし、数値に変換出来ないもの、例えば「a」という文字は、
変換出来ないので、「0」になります。
(変換は出来ないけど、型を一緒にする為に、無理やり数値にしてくれる)

変換出来ないなら、諦めて欲しいところなのですが、
頑張り屋さんなんですね、MySQLは。

という訳で、色々試して見た結果が以下の通りです。
SELECT 0 = 0;   -- True になる
SELECT 0 = 1;   -- False になる
SELECT 'a' = 'a'; -- True になる
SELECT 'a' = 'c'; -- False になる
SELECT 0 = '0';  -- True になる
SELECT 0 = 'a';  -- True になる
SELECT 1 = 'a';  -- False になる

やっぱり、「SELECT 0 = 'a'; 」が問題になりそうですね。

・他にもあった、頑張り屋さんならではの弊害

先程、「a」という文字列は数値に出来ませんでした。
では「1a」という文字列ではどうでしょう?
・・・・数値にしてくれるのです。そう、MySQLなら。

この場合、「1a」は「1」に変換されます。

こちらも色々試して見ました。
SELECT 1 = '1'; true
SELECT 1 = '1a'; true
SELECT 1 = '1abcd'; true
SELECT 1 = 'abcd1'; false
どうやら、先頭に数値が来ると、その数値に変換して比較するようです。

・でも、別にMySQLは悪くない

むしろ、頑張り屋さんで、良いと思います。

そもそも、SQLで文字列と数値を比較するような事を発生させないのが筋です。
そして、その制御はSQLに値を設定するプログラム側で行うべきだと思います。
(もっと言うと、そんな事が発生する場合、設計を見直した方が良いのかな、とも思います。)

・その他気になったところ

リファレンスマニュアルには、下記の式についても、 結果が異なると記載されています。
SELECT '18015376320243458' = 18015376320243458; true になる
SELECT '18015376320243459' = 18015376320243459; false になる
これを色々な環境で試してみたのですが、
SELECT '18015376320243458' = 18015376320243458; true
SELECT '18015376320243459' = 18015376320243459; true

間違った判定をされる事はありませんでした。

リファレンスをちゃんと読むと、
さらに、文字列から浮動小数点への変換および整数から浮動小数点への変換は、必ずしも同様に発生するとはかぎりません。整数は、CPU によって浮動小数点に変換される可能性があります。一方、文字列は、浮動小数点の乗算を伴う演算で 1 桁ずつ変換されます。 表示される結果はシステムによって異なり、コンピュータのアーキテクチャーやコンパイラのバージョンなどの要因、または最適化レベルの影響を受ける可能性があります。このような問題を回避する方法の 1 つは、値が暗黙的に浮動小数点値に変換されないように、CAST() を使用することです。
「MySQL 5.6 リファレンスマニュアル」『関数と演算子 / 式評価での型変換 / 12.2 式評価での型変換』より。
2017年1月17日 (火) 08:11 UTC
URL: https://dev.mysql.com/doc/refman/5.6/ja/type-conversion.html
なるほど、環境によって異なるのですね。

MySQL において、異なる型の比較は思わぬ結果になってしまう事がある、
という事で、注意しましょう。

2016年11月13日日曜日

MySQLの timestamp型が、なかなか厄介。

11月 13, 2016
オフィス狛 技術部です。

弊社ではしばらく使う事のなかったMySQLですが、
とあるプロジェクトで久しぶりに使い、そしてハマりました。

問題となったテーブル

まず、以下のようなCreate文を実行しました。
CREATE TABLE koma_test (
      koma_test_id bigint NOT NULL AUTO_INCREMENT,
      koam_code varchar(2) NOT NULL,
      test_count int,
      test_fee decimal(7,0),
      has_deleted boolean DEFAULT false NOT NULL,
      entry_datetime timestamp NOT NULL,
      entry_user varchar(100),
      update_datetime timestamp NOT NULL,
      update_user varchar(100),
      PRIMARY KEY (koma_test_id)
);

「entry_datetime」「update_datetime」などは、システムで良くあるタイムスタンプ系のカラムです。
日時はプログラム側で設定する事を想定しています。

発生した現象

本来、プログラム側では登録時(Insert時)のみ「entry_datetime」が設定され、
それ以降、その値は変わらない想定でした。

ところが、テーブル更新時(Update時)に「entry_datetime」が勝手に更新されていたのです。
当然最初はプログラムを疑いましたが、プログラムに問題はありませんでした。

で、ちょっとハマった後、何気なく作成されたテーブルの定義を確認すると、
「entry_datetime」に「CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP」が付いているのです。
(「update_datetime」には付いていない)

もちろん、Create文には、そのような指定はしていません。
どうやら、timestamp型には、デフォルトで「CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP」が付いてしまうようです。
※他のサイトの情報だと、NULL 許可していても、NOT NULL にもなってしまうようです。

なんて余計なお世話だ・・・とも思いましたが、
まあ、「timestamp」という属性名だし、そういう事になりますよね・・・
タイムスタンプ系の使い道で無い場合、型は「datetime」にすれば良いだけですからね。

とは言うものの、今のままでは、本来の用途に使えないので、テーブルの定義を変更します。
ALTER TABLE koma_test CHANGE entry_datetime entry_datetime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;

注意事項ですが、デフォルト値の「CURRENT_TIMESTAMP」も削除したいからといって、
ALTER TABLE koma_test CHANGE entry_datetime entry_datetime TIMESTAMP NOT NULL;
というSQLを流してしまうと、
またデフォルトの「CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP」が付いてしまいます。

MySQLを使い込んでいる方ですと、「そりゃ、そうでしょ」レベルの話なのかもしれませんが、
久しぶりに使うと、ハマってしまいますね。