💎

ruby-mysql と ruby-mysql2

2022/12/02に公開約4,500字1件のコメント

これはMySQLアドベントカレンダーとRubyアドベントカレンダーの3日目の記事です。

https://qiita.com/advent-calendar/2022/mysql
https://qiita.com/advent-calendar/2022/ruby


ruby-mysql

誰も使わないだろうけど、ruby-mysql 4.0 をリリースした。

ruby-mysql | RubyGems.org | コミュニティのGemホスティングサービス

ruby-mysql は Ruby で書かれた MySQL 用のクライアントライブラリ。

3.0 に対する大きな変更点

Mysql#query の結果の値オブジェクトのクラス

今まではプリペアドステートメント(Mysql#prepare)ではない通常のクエリ(Mysql#query)の結果の値はすべて String で返していた。
プリペアドステートメントの場合は、MySQL の型に応じたオブジェクトを返していた。
4.0 で通常のクエリでもプリペアドステートメントと同様のオブジェクトで結果を返すようにした。
あと DECIMAL と DATE の型も変更した。

MySQL type Ruby class
NULL NilClass
INT Integer
DECIMAL BigDecimal (3.0 までは String)
FLOAT, DOUBLE Float
DATE Date (3.0 までは Time)
DATETIME, TIMESTAMP Time
TIME Float (秒単位)
YEAR Integer
CHAR, VARCHAR String
BIT String
TEXT, BLOB, JSON String

3.0:

pp my.query('select 123,123.45,now(),cast(now() as date)').fetch.map{[_1, _1.class]}
#=> [["123", String],
#    ["123.45", String],
#    ["2022-11-15 00:17:11", String],
#    ["2022-11-15", String]]

4.0:

pp my.query('select 123,123.45,now(),cast(now() as date)').fetch.map{[_1, _1.class]}
#=> [[123, Integer],
#    [0.12345e3, BigDecimal],
#    [2022-11-15 00:17:17 +0900, Time],
#    [#<Date: 2022-11-15 ((2459899j,0s,0n),+0s,2299161j)>, Date]]

今までと同じように String で返すには、Mysql.new とか Mysql.connect とか Mysql#query とか Mysql#each とか Mysql#fetch とかに cast: false を指定する。

Mysql.new(cast: false).connect.query('select 123').fetch   #=> ["123"]
Mysql.new.connect(cast: false).query('select 123').fetch   #=> ["123"]
Mysql.connect(cast: false).query('select 123').fetch       #=> ["123"]
Mysql.connect.query('select 123', cast: false).fetch       #=> ["123"]
Mysql.connect.query('select 123').each(cast: false).first  #=> ["123"]
Mysql.connect.query('select 123').fetch(cast: false)       #=> ["123"]

または、Mysql.default_options を変更すると、それ以降そのプロセスで生成された Mysql オブジェクトの振る舞いが変更される。

m1 = Mysql.connect
Mysql.default_options[:cast] = false
m2 = Mysql.connect
m1.query('select 123').fetch  #=> [123]
m2.query('select 123').fetch  #=> ["123"]

Mysql::Result#each が毎回先頭から繰り返す

3.0 までは each は前回の続きから結果を返すが、4.0 では最初から繰り返す。

3.0:

res = my.query('select 1 union select 2 union select 3')
res.entries  #=> [["1"], ["2"], ["3"]]
res.entries  #=> []

res = my.query('select 1 union select 2 union select 3')
res.each.first  #=> ["1"]
res.each.first  #=> ["2"]
res.each.first  #=> ["3"]
res.each.first  #=> nil

4.0:

res = my.query('select 1 union select 2 union select 3')
res.entries  #=> [[1], [2], [3]]
res.entries  #=> [[1], [2], [3]]

res = my.query('select 1 union select 2 union select 3')
res.each.first  #=> [1]
res.each.first  #=> [1]
res.each.first  #=> [1]

3.0 と同じ振る舞いにしたいことはないと思うけど、もししたい場合は、こんな感じで:

require 'mysql'
class Mysql::Result
  def each(**opts)
    while (r = fetch(**opts))
      yield r
    end
  end
end
my = Mysql.connect
res = my.query('select 1 union select 2 union select 3')
pp res.entries  #=> [[1], [2], [3]]
pp res.entries  #=> []

その他

RSpec

テストコードは test-unit で書いてたんだけど、RSpec を使うようにした。慣れてるので。

GitHub から GitLab に移行

https://github.com/tmtm/ruby-mysql から https://gitlab.com/tmtms/ruby-mysql に移行した。なんとなく。

ruby-mysql2

mysql2 は C ライブラリの libmysqlclient を使ってるんだけど、その代わりに ruby-mysql を使うと面白いかと思って、mysql2 をベースに ruby-mysql2 を作ってみた。

[https://gitlab.com/tmtms/ruby-mysql2:embed:cite]

mysql2 のテストコードはかなり充実してて、ruby-mysql の開発にも役立った。

テストコードはだいたい通ったので、普通に mysql2 の代わりとして使えると思う。

mysql2 との非互換は、これくらい。

  • my.cnf 等を読まない
  • caching_sha2_password, mysql_native_password, sha256_password 以外の認証方式はサポートしない
  • Mysql2::EM がない

試しに ActiveRecord で使ってみようと思ったんだけど、mysql2_adapter.rb 中で gem "mysql2" と書かれてたので、ダメだった。

Sequel では普通に使えた。

まあでも、特に ruby-mysql2 を使う理由はないな。普通に mysql2 を使えばいいんだし。mysql2 に比べたらかなり遅いし。


また誰の役に立たないものを作ってしまった…。

Discussion

ruby-mysql, Sequelのアダプタとしてかなりお世話になってます!
最近だと、AWS Lambdaで依存ライブラリごとzipパッケージアップロードする方法をとるときに、pure rubyなのでとても手軽に同梱できて、助かってます♪

ログインするとコメントできます