ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【MySQL】サーバーのSQLモードを設定する

前置き

前の記事で、MySQL のテーブルで、日付型の列に、日付Zero値が入ってしまう話を書きましたが、その理由の一つが、MySQL のサーバー設定(SQLモード)が、デフォルトで日付Zero値を許容する設定になっているためです。

他のデータベースのように、日付Zero値を許さないようにするには、SQLモードを適切に設定する必要があります。

Qiita のこちらの記事を全面的に参考にさせてもらっています。
MySQLのSQLモードをstrictモードで設定する。 - Qiita

デフォルトのSQLモード(バージョン8の場合)

MySQL バージョン 8 の SQLモードのデフォルト値は、以下のようになっています。

mysql> SELECT @@global.sql_mode \G
*************************** 1. row ***************************
@@global.sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
1 row in set (0.00 sec)

先の Qiita の記事(バージョン 5.7.8)と、デフォルト値が異なっています。STRICT_TRANS_TABLES しか入ってない・・・

SQLモードを変更する

Windows 環境の場合、大きくは以下の手順で SQLモードの変更ができます。

  • C:\ProgramData\MySQL\MySQL Server 8.0 フォルダーにある、my.ini で、sql_mode の値を修正する。
  • 「サービス」を開き、MySQL80 サービスを再起動する。

Windows環境の場合、C:\ProgramData\フォルダー配下のファイルは、管理者権限が必要なので、修正するのに工夫が必要です。

my.ini の内容を修正する

my.ini の内容を変更するのですが、管理者権限でテキストエディターを開く必要があります。普通に my.ini をダブルクリックで開いても、管理者権限がないので、上書き保存ができません。

少し面倒な手順になりますが・・・

  1. メモ帳を管理者権限で開く。
    方法はいろいろありますが「メモ帳」のショートカットを右クリックして、「管理者として実行」するか、コマンドプロンプトを管理者権限で実行して、notepad で起動する、のが簡単な方法かと思います。

  2. メモ帳のメニューから「ファイル - 開く」 を選択して、C:\ProgramData\MySQL\MySQL Server 8.0\my.ini を選択して開く。

としてください。

my.ini を開いたら、sql-mode の値を設定している箇所を探して、値を修正します。

# Set the SQL mode to strict
#sql-mode="STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION"
sql-mode="TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY"

蛇足ですが、#コメントアウトできるので、元の行をコピーしてコメントアウトし、残しておきましょう。 ここでは、先の Qiita の記事の通り、TRADITIONALNO_AUTO_VALUE_ON_ZEROONLY_FULL_GROUP_BY を設定するようにします。

上書き保存したら、「サービス」を開き、MySQL80 サービスを再起動してください。(ここは分かると思うので説明しません・・・)

変更した結果・・・

MySQL にログインし直して、SQLモードがどうなってるか見てみます。

mysql> select @@global.sql_mode \G
*************************** 1. row ***************************
@@global.sql_mode: ONLY_FULL_GROUP_BY,NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,TRADITIONAL,NO_ENGINE_SUBSTITUTION
1 row in set (0.00 sec)

これも、微妙に変わってて、NO_AUTO_CREATE_USER がなくなってます。

この設定で、日付型の列に、Zero値を入れようとしてみます。

mysql> INSERT INTO datetest(id, date) VALUES (100, '0000-00-00');
ERROR 1292 (22007): Incorrect datetime value: '0000-00-00' for column 'date' at row 1
mysql> INSERT INTO datetest(id, date) VALUES (100, '0000-01-00');
ERROR 1292 (22007): Incorrect datetime value: '0000-01-00' for column 'date' at row 1
mysql> INSERT INTO datetest(id, date) VALUES (100, '0000-01-01');
Query OK, 1 row affected (0.01 sec)

月や日がZeroの日付は、Incorrect datetime value のエラーでINSERTが失敗しています。最後の年がZeroの日付は、日付としては正しいので、INSERTが成功しています。

腑に落ちないこと

MySQL :: MySQL 8.0 Reference Manual :: 5.1.11 Server SQL Modes を読んでいると、腑に落ちない点がいくつか出てきます。。。

デフォルトの sql_mode に STRICT_TRANS_TABLES が入っていること

MySQLリファレンスによると、「STRICT_TRANS_TABLES もしくは STRICT_ALL_TABLES のいずれかが有効であれば、厳密モードである」という記載があるのですが、だとすると最初から「厳密モード」で動いていた、ということになります。

しかし、MySQL 5.7 以降では、NO_ZERO_IN_DATENO_ZERO_DATESQLモードは非推奨で、厳密モードに含まれる(厳密モードになっていれば有効、という意味だと思います)という記載もあります。

であれば、最初から「厳密モード」だったなら、Zeroを含む日付は入れられないハズなのですが、、、実際は入れられました。InnoDB で生成しているので、実は確認に使ってたテーブルが非トランザクションテーブルだった、なんてこともないハズ・・・

TRADITIONALsql_mode に NO_ZERO_IN_DATENO_ZERO_DATE が入っていること

TRADITONAL sql_mode を設定すると、SQLモードに NO_ZERO_IN_DATENO_ZERO_DATE が普通に入ってきます。厳密モードに含まれるなら、なんでここに出てくるのか、とても疑問です。

実際には廃止になっていなくて、SQL 5.6 と変わってないんじゃ?と疑いたくなる感じです・・・

ONLY_FULL_GROUP_BY の意味

話が日付Zero値の話とずれますが。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.19.3 MySQL での GROUP BY の処理 に詳細が書かれているのですが、

ONLY_FULL_GROUP_BY SQLモードは、有効でない場合、「GROUP BY句に含まれない列項目を、選択リストや HAVING句、ORDER BY 句で選択することができる」というものです。

仕様の特徴としては、

  • GROUP BY句で指定していない列を選択リストに入れた場合でもエラーにならない。ただし、その列の値は不定値となる。
  • HAVING句の中で、選択リストでつけた「別名」を使用できる。
  • MAX、MIN と組み合わせると、そのグループの最大、最小となる行の項目を取得できる。
  • GROUP BY句の中で、式(SUMとか)を使うことができる。

等々、なのですが・・・どれもけっこう一般的じゃない状況な気がしています。正直、これがデフォルトのSQLモードで有効になっていない理由が良く分かりません。

何より、できるだけ標準SQL に準じてアプリを作っておいた方が、万が一 DB が変わったときにも対応がしやすいので、MySQLsql_mode はちゃんと確認して、修正しておいた方が良さそうです。

【Ruby】MySQLで日付型のデータを扱う際の注意点(でも実際はSQLモードの問題・・・)

前置き

Ruby から mysql2 gem を使って、MySQL へアクセスをいろいろ試してみよう、、、と思ったのですが、Qiita の記事で、こんなのを見つけてしまったので、こちらについていろいろ調べてみました。

Ruby mysql2 で prepareする際の注意点 - Qiita

この記事で使用したアプリのバージョン情報はこちらです。

どういうことか?

リンク元の記事の内容をまとめると、下記の通りです。

  • MySQL では、DATE型、DATETIME型のカラムには「Zero値」を入れられる。
  • MySQL側のテーブル内のデータに、日付型のZero値が含まれている場合、そのデータを prepared statement で取得しようとすると、エラー「invalid date (ArgumentError)」となってしまう。
  • prepared statement ではなく、query メソッドで取得する場合は、エラーにならず、nil として取得できる。

これって・・・けっこう重大な話なんじゃ・・・と思いつつ、詳しく調べてみました。

MySQLの日付型に入れられるデータ

MySQL では、設定がデフォルトのままの場合、通常では日付と判断されない日付を入れることができます。

  • Zero値 : 0000-00-00 00:00:00
  • 日のZero値 : 2020-12-00 00:00:00
  • 月のZero値 : 2020-00-15 00:00:00

詳しくは、MySQL のリファレンスに書いてあります。バージョンが 5.6 のものですが、バージョン 8 も同様です。 MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.3 日付と時間型

MySQL では、DATE または DATETIME カラムに、日がゼロ、または月および日がゼロである日付の格納を許可しています。これは、正確な日付がわかっていない可能性のある生年月日を格納する必要があるアプリケーションで役立ちます。この場合は、単に日付を '2009-00-00' または '2009-01-00' として格納します。このような日付を格納する場合は、DATE_SUB() や DATE_ADD() などの完全な日付を必要とする関数で正しい結果が返されることは期待しないでください。日付でゼロの月または日の部分を無効にするには、NO_ZERO_IN_DATE SQL モードを有効にします。


MySQL では、「ダミーの日付」として '0000-00-00' の「ゼロ」の値を格納できます。場合によっては、これは、NULL 値を使用するよりも便利であり、使用するデータおよびインデックススペースが少なくなります。'0000-00-00' を無効にするには、NO_ZERO_DATE SQL モードを有効にします。


・・・「ダミーの日付」として Zero値が入れられる、というのは、まぁそれも有りか、という気もしますが・・・月や日に Zero値が入れられるのって、はっきり言って「余計な仕様つけるんじゃねーよ!」とツッコみたくなる内容です。月や日が未定だったら、それに対応できるようにシステム・データベース設計するべきだし。日付型のところに、明らかに日付エラーとなる値がしれっと入る方が、よっぽど迷惑なんですけど・・・

MySQL側のデータ準備

というわけで、MySQL側でデータを準備します。今回は、テーブル名datetest に、数値型のid列、datetime型の‘date‘列を作り、確認しておきたいデータを格納します。

mysql> DESC datetest ;
+-------+----------+------+-----+---------+-------+
| Field | Type     | Null | Key | Default | Extra |
+-------+----------+------+-----+---------+-------+
| id    | int      | YES  |     | NULL    |       |
| date  | datetime | YES  |     | NULL    |       |
+-------+----------+------+-----+---------+-------+
2 rows in set (0.06 sec)
mysql> SELECT * FROM datetest ORDER BY id;
+------+---------------------+
| id   | date                |
+------+---------------------+
|    1 | 2020-11-24 21:30:50 |        # 通常の日付
|    2 | 0000-00-00 00:00:00 |        # Zero日付
|    3 | NULL                |        # Null値
|    4 | 2020-11-00 00:00:00 |        # 日のみ Zero の日付
|    5 | 2020-00-01 00:00:00 |        # 月のみ Zero の日付
|    6 | 0000-01-01 00:00:00 |        # 年のみ Zero の日付(これは日付エラーにならないデータ)
+------+---------------------+
6 rows in set (0.00 sec)

このデータを、Ruby の mysql2 を使って取得してみます。

時刻データの取得

データの取得は、query メソッドを使用するか、prepare メソッドで SQL文を準備した後、execute メソッドで実行する(prepared statement) か、の2通りがあります。

# query メソッドを使う場合
results = client.query("SELECT id, date FROM datetest WHERE id = #{id}")

# prepared statement を使う場合
statement = client.prepare("SELECT id, date FROM datetest where id = ?;")
results = statement.execute(id)    

この2つの方法で、datetest テーブルの date 列の値を取得したときの結果が、下の表のようになりました。

日付 query prepared
2020-11-24
0000-00-00 *1 ×*2
NULL 〇(nil) 〇(nil)
2020-11-00 × *3 × *4
2020-00-01 × *5 × *6
0000-01-01

結果まとめ

・・・まとめると・・・

  • query メソッド、prepared statement のどちらも、「日付 Zero値」、「月 Zero値」、「日 Zero値」は正しく取得できない。
    • 「日付 Zero値」を query メソッドで取得した場合のみ、nil値に変換され、例外は発生しないが、他の Zero値は、すべて例外が発生する。
  • query メソッド、prepared statement で、例外が発生するタイミングが異なる。また、例外の種類も違う。
    • query メソッドの例外 mysql2::Error は、結果に対して each メソッドを実行したときに発生する。
    • prepared statement の例外 ArgumentError は、execute メソッドを実行したときに発生する。

何が問題か?

問題は、大きく2点かな、と思っています。

  1. MySQL側の日付型の列に、日付として正しくないデータが入ってしまうこと
    MySQL サービスをデフォルトの状態で運用すると、日付Zero値が入ることを許容した設定になってしまいます(version 8 でも同じ)。これ自体が、他のデータベースでは普通に考えられないような仕様です。

  2. mysql2 モジュール側で、日付エラーとなるデータを正しく捌けないこと
    MySQL側で 日付Zero値が許容されていても、Ruby側のTime型では、そんな値は許容されていないので、そのまま値を取り込むこと自体、無理な訳です。日付エラーの場合は、無理やりString型で取り込む、なんてことをしたら、収拾つかなくなりそうですし。 それでも「日付型のチェックでエラーなら nil値」で取得できればよかったのですが、さすがに mysql2 モジュールではそこまで想定されてないようです。もしくは、そんな変な仕様許さない!かもしれないですが。

対策

MySQLSQLモードで、日付Zero値を許容しない設定に変更する

OracleSQL Server など、他のデータベースでは、日付型の列に Zero値なんて入らないわけで。そのように MySQL でも動作するように、SQLモードを変更してしまいます。

というか、特別な理由がない限り、そうした方が絶対に良い、と私は思います。 変更方法については、次の記事で。

日付型の列に、すでに日付Zero値が入ってしまっていて、どうしても変えられない場合

この場合は、アプリケーション側で何とかするしかないわけで・・・

  • SQL文で日付型の列を取得する際、文字列で取得するようにする。
SELECT DATE_FORMAT( date, '%Y%m%d') as date from datetest;

上記のように、MySQL側で文字列に変換したデータを取得するようにすれば、日付Zero値も文字列として取得することができるので、あとはアプリケーション側でどうとでも捌くことができるようになります。

でも、これはあくまでデータベース側の設定が変えられない事情がある場合であって、通常は MySQLSQLモードを変更して、システム設計する方が良いはずです。


*1:nil値として取得

*2:executeメソッドを実行したときに例外:ArgumentError:invalid date

*3:queryの結果に対して eachメソッドを実行したときに例外:Mysql2::Error:invaild date in field 'date'

*4:executeメソッドを実行したときに例外:ArgumentError:mday out of range

*5:queryの結果に対して eachメソッドを実行したときに例外:Mysql2::Error:invaild date in field 'date'

*6:executeメソッドを実行したときに例外:ArgumentError:mon out of range

【Ruby】MySQL に接続してみる その2

前置き

【Ruby】MySQL に接続してみる その1 - ぎょーぼのぶろぐ の記事の続きです。

前回、MySQL側のデータの準備を行いましたので、今回は、Ruby側から MySQL に接続して、データを参照してみます。

MySQL に接続するための gem をインストールする。

Ruby から MySQL へ接続するためには、Ruby用の MySQL クライアントが必要です。

2020/11/18 時点で、いろいろ種類があるみたいですが、「MySQL2」という gem が最も使われているようなので、こちらを使用します。

  • 一番それっぽい名前の「MySQL」gem は、エラーでインストールできませんでした。gem のアップデートも 2013年でストップしているので、皆さん「MySQL2」へ移っているものと思われます。

「MySQL2」gem のインストールは、gem install コマンドを使えば OK です。

> gem install mysql2

引数なしでインストールすると、バージョンは 「mysql2 (0.5.3 x64-mingw32)」となりました。 *1 ・・・メジャーバージョンが 0 なのはなんでだろ?

Ruby コードから、MySQL のデータを取得してみる

「Mysql2」 gem の使い方は、製作者の方が github 上に公開しています。

GitHub - brianmario/mysql2: A modern, simple and very fast Mysql library for Ruby - binding to libmysql

こちらを参考に、テーブルのデータを取得して表示するだけの、単純な Ruby のコードを書いてみます。手順としては、、、

  1. mysql2 を require する。
  2. Mysql2::Client.new でコネクションオブジェクトを生成する。
  3. コネクションオブジェクトの query メソッドに SQL文を送って、結果を取得する。
  4. 結果を表示

実際のコードは下のような感じです。ファイル名は「mysql_test.rb」です。

require 'mysql2'

client = Mysql2::Client.new(host: 'localhost', username: 'gyobo', password: 'password', encoding: 'utf8mb4', database: 'world')

results= client.query('select * from country;')

results.each do |row|
    puts "ID : #{row["id"]}, Name: #{row["name"]}"
end

これで実行してみると・・・下のようにエラーが出てしまいます。

>ruby mysql_test.rb
Traceback (most recent call last):
        3: from mysql_test.rb:3:in `<main>'
        2: from mysql_test.rb:3:in `new'
        1: from C:/Ruby/Ruby26-x64/lib/ruby/gems/2.6.0/gems/mysql2-0.5.3-x64-mingw32/lib/mysql2/client.rb:90:in `initialize'
 (Mysql2::Error)64/lib/ruby/gems/2.6.0/gems/mysql2-0.5.3-x64-mingw32/lib/mysql2/client.rb:90:in `connect': Authentication plugin 'caching_sha2_password' cannot be loaded: ?w?肳?ꂽ???W???[??????????܂???B

エラーの原因:「mysql2」gem が caching_sha2_password に対応してないから

ポイントは、エラー文言の中の「Authentication plugin 'caching_sha2_password' cannot be loaded」の部分です。認証プラグイン'caching_sha2_password'がロードできない・・・

MySQL は、ユーザー認証の際に「認証プラグイン」というのを使うそうです。MySQL 5 のときは「mysql_native_password」がデフォルトだったのですが、MySQL 8 になってから「caching_sha2_password」というのが追加され、ユーザーの新規作成時、認証プラグインを指定しない場合は、デフォルトで「caching_sha2_password」となるそうです。そして、、、MySQLクライアントで「caching_sha2_password」に対応しているものが Ruby ではまだ無い(2020年11月現在)、、、ということみたいです。

「caching_sha2_password」に対応しているMySQLクライアントは、以下のリンク先で確認できます。

MySQL :: MySQL 8.0 Reference Manual :: 2.11.4 Changes in MySQL 8.0

このため、Ruby から MySQL 8 へ接続するためには、MySQL に接続するユーザーの認証プラグインをあらかじめ、「mysql_native_password」にしておく必要があります。。。

というわけで、MySQL にログインして、ユーザーの認証プラグインを「mysql_native_password」に変更します。

mysql> SELECT user, plugin FROM mysql.user WHERE user='gyobo';
+------------------+-----------------------+
| user             | plugin                |
+------------------+-----------------------+
| gyobo            | caching_sha2_password |
+------------------+-----------------------+
1 rows in set (0.00 sec)

mysql> ALTER USER gyobo@localhost IDENTIFIED WITH mysql_native_password BY 'password';
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT user, plugin FROM mysql.user WHERE user='gyobo';
+------------------+-----------------------+
| user             | plugin                |
+------------------+-----------------------+
| gyobo            | mysql_native_password |
+------------------+-----------------------+
1 rows in set (0.00 sec)

こうして再度、実行してみると、、、

>ruby mysql_test.rb
ID : 1, Name: 日本
ID : 2, Name: アメリカ
ID : 3, Name: イギリス

データを取得することができました。

補足?蛇足?

ネットでいろいろ調べてみると、MySQL でユーザーを作成する際のデフォルトの認証プラグインを「mysql_native_password」に変更したらいい、という記事も見かけました。が、個人的にはそれはしない方がいいと思います。

理由は、「caching_sha2_password」 の方が、「mysql_native_password」よりもセキュリティ面では良いらしいから。

基本は、「caching_sha2_password」を使うようにして、今回のように「caching_sha2_passwordが対応してないから使えない」という場合に、必要な範囲で「mysql_native_password」に変更するようにすべき、と思われます。

*1:2020年11月18日現在です。

【Ruby】MySQL に接続してみる その1

前置き

Ruby から、MySQL データベースに接続してみます。

今回の環境は、Windows10、Ruby 2.6.6、MySQL 8.0.22 です。

C:\Users\gyobo>ruby --version
ruby 2.6.6p146 (2020-03-31 revision 67876) [x64-mingw32]
C:\Users\gyobo>mysql --version
mysql  Ver 8.0.22 for Win64 on x86_64 (MySQL Community Server - GPL)

MySQL の準備

Windows版の MySQL のインストールについては、ネットに様々、情報が出ていますので、そちらを参考にセットアップしてください。例えばこことか。Windows 版 MySQL インストール手順 - Qiita

セットアップするのは、「MySQL Community Server」です。

導入の際、「MySQL for Visual Studio」とか「Connector/Python」とか、使わないものは入れないでいいです。「Workbench」はコマンド苦手な人には便利なので、入れていいと思います。

インストール後、コマンドプロンプト上で、mysql --version としたときにバージョン情報が出ない場合は、パスがちゃんと通ってませんので、環境変数のパスを追加しましょう。デフォルトでインストールしているなら、C:\Program Files\MySQL\MySQL Server 8.0\bin です。

MySQL 側の準備

Ruby でアクセスする情報を、MySQL側に準備しておきます。

今回は、インストール時に「gyobo」の名前でユーザーを作っておいたので、そのユーザーでログインし、作っていきます。

とりあえず、ログインして・・・

C:\Users\gyobo>mysql -u gyobo -p
Enter password: ********
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.22 MySQL Community Server - GPL

Copyright (c) 2000, 2020, 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.

world という名前のデータベースを作ってみます。今回はこだわりがないので、名前だけ指定して生成します。

mysql> CREATE DATABASE world;
Query OK, 1 row affected (0.01 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| world              |
+--------------------+
5 rows in set (0.00 sec)

MySQL 8.0 では、デフォルトの文字コードutf8mb4、照合順序は utf8mb4_0900_ai_ci になっています。

mysql> SELECT @@character_set_database, @@collation_database;
+--------------------------+----------------------+
| @@character_set_database | @@collation_database |
+--------------------------+----------------------+
| utf8mb4                  | utf8mb4_0900_ai_ci   |
+--------------------------+----------------------+
1 row in set (0.00 sec)

utf8mb4 は、絵文字などの4バイト文字が使える文字コードです。昔、タブレットからも入力があるシステムなのに、誤って utf8 でデータベース、テーブルを作ってしまって、開発の後半になってから「絵文字どうしよう・・・」になった思い出がありますが、デフォルトになってくれたので、その心配はなくなっています。

照合順序は、文字の比較や並べ替えに使用するルールです。utf8mb4_0900_ai_ci は、文字コードutf8mb4 で、Accent Insensitive(アクセント無視)、Case Insensitive(大文字小文字無視)、ということになります。

続いて、テーブルを作成します。USE句を使って、デフォルトのデータベースを world に変更して、CREATE TABLE文でテーブルを作成します。今回は、キーとか制約、インデックスのこだわりもないので、列名と型の指定だけで作成します。

mysql> USE world
Database changed
mysql> CREATE TABLE country(id INT, name VARCHAR(20));
Query OK, 0 rows affected (0.05 sec)

mysql> DESC country;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int         | YES  |     | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

適当に、データも入れておきます。

mysql> INSERT INTO country(id, name) VALUES (1, '日本');
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO country(id, name) VALUES (2, 'アメリカ');
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO country(id, name) VALUES (3, 'イギリス');
Query OK, 1 row affected (0.01 sec)

mysql> select * from country;
+------+----------+
| id   | name     |
+------+----------+
|    1 | 日本     |
|    2 | アメリカ |
|    3 | イギリス |
+------+----------+
3 rows in set (0.00 sec)

このデータに、Ruby のコードからアクセスしてみようと思います。

少し長くなってきたので、次の記事に続きます。

※ DB側の設定はもう一つあって、Ruby からアクセスする際、ユーザー認証の「認証プラグイン」についての設定を変更する必要があります。そちらは続きの記事で。

【Ruby】require と require_relative

前置き

requirerequire_relative の違いと注意点についてです。

require

require は、引数で与えられたファイルパスのファイルを一度だけ呼び出します。このとき、引数のパスは、絶対パスでも相対パスでもOKで、相対パスの場合は、requireが呼ばれた時点でのカレントディレクトリからの相対パスになります。

C:\gyobo\test\ ディレクトリに、a.rbreq.rb の2つのファイルを置き、a.rb ファイルには、

require "req.rb"

とし、req.rb は、

p "required `req.rb`!"

と書いておきます。これで、カレントディレクトリを変えたりして a.rb を実行してみると、、、

C:\gyobo\test>ruby a.rb       # => "required `req.rb`!"

C:\gyobo>test\ruby a.rb       # => ruby: No such file or directory -- a.rb (LoadError)

カレントディレクトリが合っていないと、ファイルが見つからずに LoadError となってしまいます。

require_relative

require_relative も、引数で与えられたファイルパスのファイルを一度だけ呼び出しますが、相対パスが与えられた場合、実行しているファイルのある場所からの相対パスとして解釈してくれます。

先ほどと同じように、C:\gyobo\test\ ディレクトリに、a.rbreq.rb の2つのファイルを置いた状態で、a.rb ファイルの方を、

require_relative "req.rb"

に書きかえて、同じように実行してみると、、、

C:\gyobo\test>ruby a.rb       # => "required `req.rb`!"

C:\gyobo>test\ruby a.rb       # => "required `req.rb`!"

となり、実行するカレントディレクトリの場所に関係が無くなり、実行しているファイルの相対パスで呼び出せるようになります。

require_relative の注意点

require_relative では、__dir__と同様にシンボリックリンクを解決します。

例えば、C:\gyobo\test2\ ディレクトリに、sl_a という名前で C:\gyobo\test\a.rb へのシンボリックリンクを作成します。その状態で、sl_a を実行すると、、、

C:\gyobo\test2>ruby sl_a       # => "required `req.rb`!"

test2 ディレクトリにある sl_a ファイルを実行していますが、ちゃんと test\req.rb が呼び出されています。また、req.rbtest2 ディレクトリに移動させて実行すると、、、

C:\gyobo\test2>ruby sl_a
Traceback (most recent call last):
        1: from sl_a:1:in `<main>'
sl_a:1:in `require_relative': cannot load such file -- C:/gyobo/test/req.rb (LoadError)

実行しているのは sl_a ですが、C:/gyobo/test/req.rb を探しに行っているのが分かります。require_relativeは、あくまで実体ファイルのある場所からの相対パスで探索する、ということを忘れないように注意しましょう。

【Ruby】数値に3桁区切り文字をつける

前置き

Ruby で数値に3桁区切りを付加した文字列を取得する Tips です。

やり方

普通に考えると、

  1. 下桁から3桁ずつに区切って、
  2. 区切り文字を入れる。
  3. それを、桁がなくなるまで繰り返す。

という手順になりますが、Ruby だと一行でできます。(他の言語でもできるんだとおもいますが)

例えば、数値1234567 に区切り文字(仮にカンマ)を付与した文字列を取得する場合、

1234567.to_s.reverse.scan(/.{1,3}/).join(",").reverse      # => "1,234,567"

何をやっているかというと、

  • まず、to_s で文字列に変換 => "1234567"
  • 順序を反転する => "7654321"
  • scan メソッドで、正規表現にマッチした部分を繰り返して配列を取得。ここでの正規表現は「なんでもいいから少なくとも1文字、多くて3文字」にマッチする => { "765", "432", "1" }
  • join メソッドで、指定した文字列を区切りにして配列を結合 => "765,432,1"
  • 順序を反転する => "1,234,567"

となります。メソッドチェーンでつなげば、一行で書くことができます。

メソッド化

メソッド化するなら、区切り文字も引数で変えられるようにして、以下のような感じにしておけば良いように思います。メソッド名はお好みで。

def add_separaor(str, separator = ",")
    return str.to_s.reverse.scan(/.{1,3}/).join(separator.to_s).reverse
end

Rails 環境なら

Rails 環境なら、number_to_currency メソッドという便利なメソッドが用意されています。区切り文字だけでなく、単位やマイナスの場合の表現も変えられたり、多言語化もできるので、素直にこちらを使った方が良いです。

詳しい説明はこちらの記事を見てください。

https://qiita.com/azusanakano/items/c2e73521d5a4bdfa73d6

【Ruby2.6】__FILE__ を使ってできること

前置き

前回の続きです。 __FILE__ を使って色々な情報を取得してみます。

実行している「ファイル名のみ」を取得する

最後のパス区切り文字以降がファイル名になるので、それを取り出すメソッドを使う、正規表現で抽出したり、文字列加工したりすることで取得することができます。

下記、取り出し例です。C:\gyobo\test\a.rb 内で実行すると全て、"a.rb"文字列を返します。__FILE__の中身が絶対パスか、相対パスかにかかわらず、同じ結果が返ります。

# File.basename を使う。
File.basename(__FILE__)            

# "/" で配列に分割後、配列要素の最後のものを取る。
__FILE__.split("/")[-1]

# 最後の"/"から最後までを抽出。先頭に"/" が残るので、それを削除する。 
# "/" が無い場合は、そのままの値を返す。
__FILE__.include?("/") ? __FILE__.match(/\/[^\/]*$/).to_s.delete("/") : __FILE__

素直に File.basename を使うのが一番良いと思います。何かの理由でFileオブジェクトが使えない場合は、split を使うのが次善策でしょうか。

実行しているファイルの絶対パスを取得する

前回の記事でも書きましたが、__FILE__相対パスが入る場合、相対パスのベースとなるディレクトリの情報は入っていません。ですので、カレントディレクトリの情報と合わせる必要があります。

絶対パスを自力で生成しようとする場合、Dir.pwd 等でカレントディレクトリを取得し、join で結合することになりますが、__FILE__絶対パスかどうかの確認が必要になり、また__FILE__ の中に、親ディレクト.. やカレントディレクト. 等が入っている場合、ちゃんと処理をしないとそのまま入ってきてしまいます。

File.expand_path を使えば、第一引数が絶対パスであれば、第二引数を指定しなければ、カレントディレクトリを基準に絶対パスへの展開を行ってくれます。また、... の処理も合わせて行ってくれるので、素直に File.expand_path を使った方が良いです。

下記、そのまんまですが、取り出し例です。ファイルの絶対パスは、C:\gyobo\test\a.rb です。

File.expand_path(__FILE__)      # => "C:\gyobo\test\a.rb"

注意点ですが、絶対パスを取得する場合は、取得前にカレントディレクトリの変更(Dir.chdirなど)をしてしまうと、正しく取得できません。

p __FILE__                      # => "a.rb"
File.expand_path(__FILE__)      # => "C:\gyobo\test\a.rb"
Dir.chdir("C:\\")
File.expand_path(__FILE__)      # => "C:\a.rb"      移動したカレントディレクトリからのパスを生成してしまう。

実行しているファイルがあるディレクトリを取得する。

実行しているファイルの絶対パスを取得した後、絶対パスディレクトリ部分を取得すればいいです。ですが、File.expand_pathだけを使う方法もあります。

# ファイルの絶対パスからディレクトリを取得する。
File.dirname(File.expand_path(__FILE__))      # => "C:\gyobo\test"

# File.expand_path だけを使っても取れる。第一引数だけ注意。
p File.expand_path("..", __FILE__)         # => "C:\gyobo\test"

File.expand_path の方は、なんで第一引数が親ディレクト..指定なのか?これは、File.expand_path の特性を利用しています。

File.expand_path メソッドは、「第一引数のパスを、第二引数のパスを基準にして絶対パスに展開」します。第二引数も相対パスの場合、カレントディレクトリを基準にして、第二引数、第一引数のパスを絶対パスに展開してくれます。

  • 第二引数に__FILE__を指定した場合、まず第二引数がカレントディレクトリを基準にして展開され、C:\gyobo\test\a.rb となります。
  • ですが、第二引数にはディレクトリが来ることが想定されているので、このパスは C:\gyobo\test\a.rb\ 、つまりa.rbはファイルではなくディレクトリである、と解釈されます。
  • ですので、a.rbディレクトリを基準にして一つ上のディレクトリが、ファイルのあるディレクトリとなるので、第一引数が親ディレクト..の指定となります。

この特性を利用すると、実行しているファイルと同じディレクトリにあるlibディレクトリのパスを取得したい場合は、下記のようになります。

# 実行ファイルと同じディレクトリにある"lib"ディレクトリの絶対パスを取得する。
p File.expand_path("..\lib", __FILE__)         # => "C:\gyobo\test\lib"

便利なのですが、見た目上、一階層ずれるので、一瞬間違っているように見えることが注意点です。また、カレントディレクトリを基準にして取得するので、前項と同様に、取得前にカレントディレクトリの変更(Dir.chdirなど)をしてしまうと、正しく取得できなくなります。

もう一つの方法として、__dir__ メソッドでも取得することができます。(ruby 2.0 以上?)

p __dir__           # => "C:\gyobo\test\"

こちらは、カレントディレクトリの変更をしても、元のディレクトリが正しく取得できるので、バージョンに問題がなければ、こちらを使う方がおすすめだと思います。

ただ、こちらも注意点があり、リファレンスマニュアルの__dir__ の項目 にもある通り、シンボリックリンクを解決」したパスを返してきます。ですので、パスの中にシンボリックリンクが含まれている場合、File.expand_path__dir__ の結果が違ってきます。

例えば、実体ファイルが C:\gyobo\test\a.rb で、C:\gyobo\test2\link_testC:\gyobo\test へのシンボリックリンクとして作成して、C:\gyobo\test2\link_test\ をカレントディレクトリにして、a.rb を実行します。

p File.expand_path("..", __FILE__)       # => "C:\gyobo\test2\link_test"
p __dir__                                # => "C:\gyobo\test"

これは、実行ファイルa.rb自体をシンボリックリンクにした場合も同様です。

Windows環境の場合は、そもそもシンボリックリンクを作ること自体がけっこう特殊な状況なので、問題はより起きにくいと思いますが、linux系で、シンボリックリンク生成をする可能性がある場合は、要注意です。

まとめ

  • 実行している「ファイル名のみ」を取得する
    • File.basename(__FILE__)
  • 実行しているファイルの絶対パスを取得する
    • File.expand_path(__FILE__)
  • 実行しているファイルのあるディレクトリの絶対パスを取得する
    • File.expand_path("..", __FILE__)
    • __dir__

注意点としては、2点です。