前人未踏の領域へ WEB・インフラ・プログラミング全般編

フロントエンド、バックエンド、インフラ、言語など、アプリ開発、IOT以外の記録

RSpecでvalidates_dateの日付テストがうまくいかない

課題

当日の日付との比較を行う処理を validates_timelinesson_or_before を使って記述しているが、RSpecにテストを記述する際に日付の固定がうまくできず( Timecoptravel_to もNG )、現在日時との比較になってしまう。

  # 未来日の入力はエラーとする
  validates_date :date, on_or_before: Time.zone.now.to_date

原因

日付固定の外で対象のクラスが読み込まれてしまっている。

RSpec.describe Hoge, type: :model do
  before(:each) do
    # Timecop.freeze(Time.zone.local(2019, 10, 10))
    travel_to Time.zone.local(2019, 10, 10)
  end

ファイルの前後に日付をセットしていたので内部で実行されるクラスには固定した日時が採用されるかと思っていたが、RSpec.describe Hoge の行が評価される時点で Hogeの読み込みが行われ、validatesに定義している処理は時間を固定する前の値で評価されてしまっていたようだ。

対応(旧)

クラスを引数にするから評価されてしまうからそれを回避しよう、ということで文字列にしてみた。

RSpec.describe 'Hoge', type: :model do

この書き方だと何か別な問題があるのかもしれないがテストは通るようになった。

対応(新)

validates のところで直接時刻評価を記述しているから評価されてしまうので、メソッドに退避してみたら上手くいった。

RSpec.describe  Hoge, type: :model do

validate :day_after_tomorrow
private

def day_after_tomorrow
  unless errors.include? :date
    validates_date :date, on_or_before: Time.zone.now.to_date, allow_blank: true
  end
end

ちなみにメソッド名と中身は一致してない。

特定のタグをすべて含むアイテムを検索するSQL

全文検索エンジンとかを使わない普通のRDBの従来のテーブル構成の場合。
気づくまで時間がかかったのでメモっておく。ER図もないので分かりにくいけど。。

課題

あるアイテムに複数のタグが関連テーブル経由の外部テーブルとして紐づいていいる。 選択されたタグをすべて含むアイテムのみを抽出したい。どうすればよいか。

対応

1 まず対象となるタグIDを持つタグを得る

select id from tags where name in ('tag1', 'tag2')

2 そのタグIDを持つアイテムタグ関連テーブルを得る

select item_id from items_tags
   where tag_id in ( select id from tags where name in ('tag1', 'tag2') )

3 全てのタグを両方含む必要があるので、item_idでグルーピングしてカウントがタグの数と一致したものみ抽出する

select item_id from items_tags
where tag_id in ( select id from tags where name in ('tag1', 'tag2') )
group by item_id having count(item_id) = 2)

4 最後に得られたitem_idでitemテーブルにクエリーを投げればOK

select * from items
where id in 
 ( select item_id from items_tags
   where tag_id in ( select id from tags where name in ('tag1', 'tag2') )
   group by item_id having count(item_id) = 2)

LinuxベースのDockerからMySQL 8.0に接続するための記述

課題

MySQL8にDocker上に構築したRailsからアクセスしようとしたら以下のようなエラーが発生して接続できなかった。

Plugin caching_sha2_password could not be loaded: /usr/lib/x86_64-linux-gnu/mariadb19/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory

注意点

本記事は caching_sha2_password 問題でハマった人向けであるが、Amazon RDS for MySQL 8.0は mysql_native_password なのでAWSで運用してしようとしている人は異なる対応が必要なので注意(参考記事を参照のこと)

原因

8.0.4からMySQLのデフォルトの認証プラグインcaching_sha2_password に変更になった。
LinuxではMySQLクライアントとしてMySQL互換のMariaDBが使用されるため、mysql_native_password で接続しようとしてエラーとなっている

対応

記事での解説範囲。とりあえずこんな感じで接続できるようになった。

FROM ruby:2.6.5

WORKDIR /tmp
RUN apt update && apt install -y lsb-release \ 
    && apt remove -y libmariadb-dev-compat libmariadb-dev

RUN wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-common_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/libmysqlclient21_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-community-client-core_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-community-client_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/libmysqlclient-dev_8.0.18-1debian10_amd64.deb

RUN dpkg -i mysql-common_8.0.18-1debian10_amd64.deb \
    libmysqlclient21_8.0.18-1debian10_amd64.deb \
    mysql-community-client-core_8.0.18-1debian10_amd64.deb \
    mysql-community-client_8.0.18-1debian10_amd64.deb \
    libmysqlclient-dev_8.0.18-1debian10_amd64.deb

解説

対策は2案

  1. MySQL8対応のディストリビューションを使ってビルドする
  2. Linux上にMySQL8系のライブラリをインストールする

1はUbuntuのeoanがMySQL8に対応していたのでそれをビルドイメージにすればいけそうな気がする。 今回はDockerHubにあるRubyイメージからRails用イメージを構築していきたかったので2で考えてみる。

Dockerファイルを作る

1. ベースイメージ

Rubyのベースのディストリビューションもいろいろあるのだけど標準のにしてみた。このimageはDevian 10 busterをベースにしている。

FROM ruby:2.6.5

2. 依存ライブラリの追加と競合ライブラリの削除

lsb-release がないと怒られるので追加する。 同時に、mariadb系のライブラリ系が残っているとmysql8系のライブラリがコンフリクトして入れられないので削除する

libmariadb3mariadb-common は削るとRails(mysql2)から接続できなくなるので注意。ただしmysqlコマンドはなくても通る。

RUN apt update && apt install -y lsb-release \ 
    && apt remove -y libmariadb-dev-compat libmariadb-dev

MySQL 8のライブラリをダウンロード

最新バージョンのパッケージとなるとaptでは配布されていないため、 MySQLのホームページからクライアント系のパッケージをダウンロードしてインストールする。

dev.mysql.com

必要なものは以下の通り。クライアント系は全部入れておく。もしかしたら不要なものも混ざっているかもしれない。

RUN wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-common_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/libmysqlclient21_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-community-client-core_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-community-client_8.0.18-1debian10_amd64.deb \
    https://dev.mysql.com/get/Downloads/MySQL-8.0/libmysqlclient-dev_8.0.18-1debian10_amd64.deb

dpkgコマンドで入れる。

RUN dpkg -i mysql-common_8.0.18-1debian10_amd64.deb \
    libmysqlclient21_8.0.18-1debian10_amd64.deb \
    mysql-community-client-core_8.0.18-1debian10_amd64.deb \
    mysql-community-client_8.0.18-1debian10_amd64.deb \
    libmysqlclient-dev_8.0.18-1debian10_amd64.deb

以上。 あとは普通にRaisのDockerfileを作ればよい(省略)。
安易に mysql_native_password に逃げたくなかったので時間をかけて調べたものの、caching_sha2_passwordAWSで使えないということが判明して以降はただの意地であった。

参考

IdeaVimプラグインを使いつつTabキーによるインデントを可能にする

課題

JetBrailsのIDEにIdeaVimプラグインをインストールしているとエディタで複数行選択してからTabキーでインデントさせる処理が動作しない

対応

IdeaVimはVimでいうところの vimrc に相当する ideavimrc に対応しているらしい。.ideavimrc を起動時に読み込んでくれるのでそちらを編集して機能を追加する

$ vim ~/.ideavimrc
nnoremap <Tab> >>_
nnoremap <S-Tab> <<_
inoremap <S-Tab> <C-D>
vnoremap <Tab> >gv
vnoremap <S-Tab> <gv
$ source ~/.ideavimrc

上記は調べてないけどVimの設定周りの常識テクなのかもしれない。 またVim使いならVimで行選択してインデントする方法(V>>)をご存知だろうからそれでも問題ないが、
自分のような既存のショートカットも含めて使用する、いいとこ取りのVimユーザーからすると連続インデントなどのときにTabキー使えた方が便利。

参考

stackoverflow.com

schema.rbが実行エラーになる

課題

MySQLの生成カラムを使ったテーブル定義を行ったところ、構文内に含まれる文字列が schema.rbでエスケープされ、SQL実行時にエラーとなってしまう。

やりたかったこと(例)

年と月のカラムがあるのでdate型の生成カラムを作ってみた。

ALTER TABLE t1  ADD COLUMN c1 INT 
   GENERATED ALWAYS AS (str_to_date(concat(`year`, _utf8mb3'-', `month`, _utf8mb3'-', 1), _utf8mb3'%Y-%m-%d'))

すると以下のような感じに変換される。シングルクウォートがエスケープされているのがわかる。

create_table t1 |t|
  t.virtual "c1", type: :date, as: "str_to_date(concat(`year`,_utf8mb3\\'-\\',`month`,_utf8mb3\\'-\\',1),_utf8mb3\\'%Y-%m-%d\\')"
end

まあこんな変なことしようとしているのが悪いというのは別な話であるとして。

対応

schema.rbを編集したり、生成されるクエリーをエスケープさせない対応は難しそうなので、データベーススキーマをファイルに書き出す際のフォーマットをデフォルトのRubyからSQLに変更することで対応。

#config/application.rb

class Application < Rails::Application
  config.active_record.schema_format = :sql
end

スキーマの書き出しは以下の通り。schema ではなく structure になっているのがポイント。 db/structure.sql が生成される。

db:structure:dump

schema.rbは削除してしまって問題ないだろう。

参考

blog.toshimaru.net

オブジェクトの配列から文字列の配列を作る

課題

Rubyでこんな感じのオブジェクト配列があったとする。

        "tags": [
            {
                "id": 7,
                "name": "tag1"
            },
            {
                "id": 8,
                "name": "tag2"
            },
            {
                "id": 9,
                "name": "tag3"
            }
        ],

これをStringの配列で返却したい。

    "tags": [
        "tag1",
        "tag2",
        "tag3"
    ]

対応

collectを使い、要素内の値を抽出して新しい配列を作る

json.tags do
  json.array! @transaction.tags.collect {|t| t.name}
end

参考

rails jbuilder - just an array of strings - Stack Overflow

RRULEに関するメモ書き

カレンダーの予定のような繰り返しのデータ定義を実装するにあたり、RFC5545(旧RFC2445)のRRULEというのがあるのを知ったので それについて調べたメモ。中でも 3.3.10. Recurrence Rule についてのみ取り扱っている。

比べると、記述方法が若干変わっていた。RFC5545の方が説明が増えて読みやすくなっている。
今回は繰り返しのデータ定義方法の参考にしたいだけなのでどちらでも特に問題はない。

書式

/ は ORの意味

   recur           = recur-rule-part *( ";" recur-rule-part )
  • recur-rule-part; で区切りながら連続表記する。
  • ルールの順序は考慮しない
  • FREQ ルールは必須。ただし複数回登場してはならない
  • UNTIL または COUNTはオプション。ただし、UNTIL と COUNT は同時には指定できない
  • それ以外はオプションで指定可能。ただし複数回登場してはならない
recur-rule-part = ( "FREQ" "=" freq )
                       / ( "UNTIL" "=" enddate )
                       / ( "COUNT" "=" 1*DIGIT )
                       / ( "INTERVAL" "=" 1*DIGIT )
                       / ( "BYSECOND" "=" byseclist )
                       / ( "BYMINUTE" "=" byminlist )
                       / ( "BYHOUR" "=" byhrlist )
                       / ( "BYDAY" "=" bywdaylist )
                       / ( "BYMONTHDAY" "=" bymodaylist )
                       / ( "BYYEARDAY" "=" byyrdaylist )
                       / ( "BYWEEKNO" "=" bywknolist )
                       / ( "BYMONTH" "=" bymolist )
                       / ( "BYSETPOS" "=" bysplist )
                       / ( "WKST" "=" weekday )

ぱっと見わかりにくいのだけど、続く文章で各項目を説明している。

freq(周期)

  • SECONDLY / MINUTELY / HOURLY / DAILY / WEEKLY / MONTHLY / YEARLY のいずれか

enddate (終了日)

日付または日時

enddate     = date / date-time

byseclist (秒指定)

秒を指定。秒をカンマ区切りで複数回指定できる。

byseclist   = ( seconds *("," seconds) )

seconds     = 1*2DIGIT       ;0 to 60 1から2桁の整数。0 〜 60まで。0と60の違いは不明。

byminlist (分指定)

分を指定。分をカンマ区切りで複数回指定できる

byminlist   = ( minutes *("," minutes) )

minutes     = 1*2DIGIT       ;0 to 59  #1から2桁の整数。0 〜 59まで

byhrlist (時間指定)

時間を指定。カンマ区切りで複数回指定できる。

byhrlist    = ( hour *("," hour) )

hour        = 1*2DIGIT       ;0 to 23 # 1から2桁の整数。0〜23まで

bywdaylist(曜日指定)

weekdaynumをカンマ区切りで複数回指定できる。

bywdaylist  = ( weekdaynum *("," weekdaynum) )

weekdaynum  = [[plus / minus] ordwk] weekday # その年の何週目の何曜日か。
plus         = "+"
minus      = "-"
ordwk      = 1*2DIGIT       ;1 to 53 # 年の何週目か。1または2桁。1から53まで
weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
(それぞれ SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY に対応)

bymodaylist (月日指定)

月の何日目かを指定する。複数回指定可能

bymodaylist = ( monthdaynum *("," monthdaynum) )

monthdaynum = [plus / minus] ordmoday # 何日目かを指定する
ordmoday    = 1*2DIGIT       ;1 to 31 # 1または2桁。1から31まで

byyrdaylist(年の何日目かを指定)

yeardaynum を複数回指定できる

byyrdaylist = ( yeardaynum *("," yeardaynum) )

yeardaynum  = [plus / minus] ordyrday # 年の何日目か
ordyrday    = 1*3DIGIT      ;1 to 366 # 1桁から3桁。 1から366まで

bywknolist(何番目の週か)

bywknolist  = ( weeknum *("," weeknum) )

weeknum     = [plus / minus] ordwk
ordwk #1または2桁。1から53まで

bymolist(月指定)

bymolist    = ( monthnum *("," monthnum) ) # 月をカンマ区切りで複数回

monthnum    = 1*2DIGIT       ;1 to 12 #1桁または2桁。 1から12

bysplist(日の位置指定)

bysplist    = ( setposday *("," setposday) )  # 何番目の日かを指定

setposday   = yeardaynum
yeardaynum  = [plus / minus] ordyrday # 年の何日目か
ordyrday    = 1*3DIGIT      ;1 to 366 # 1桁から3桁。 1から366まで

FREQ

  • SECONDLY、1秒以上の間隔に基づいて繰り返しイベントを指定
  • MINUTELY、1分以上の間隔に基づいて繰り返しイベントを指定
  • HOURLY、1時間以上の間隔に基づいて繰り返しイベントを指定
  • DAILY、1日以上の間隔に基づいて繰り返しイベントを指定
  • WEEKLY、1週間以上の間隔に基づいて繰り返しイベントを指定
  • MONTHLY、1ヶ月以上の間隔に基づいて繰り返しイベントを指定
  • YEARLY、1年以上の間隔に基づいて繰り返しイベントを指定

INTERVAL

どの間隔で繰り返すかを指定。デフォルトは1で、毎秒、毎分、毎時、毎日、毎週、毎月、毎年を表す。 例えば FREQ=DAILY;INTERVAL=8 は8日ごとを表す。

UNTIL

  • 繰り返しの終了日('DATE')または終了日時(DATE-TIME)を表す。
  • UNTILルール部は、繰り返しルールの境界を含むDATEまたはDATE-TIME値を包括的に定義する
  • UNTIL で指定された値が指定された繰り返しと同期している場合は、この'DATE'または'DATE-TIME'が繰り返しの最後のインスタンスになる
  • 'UNTIL'ルールパートの値は DTSTARTプロパティと同じ値型を持たなければならない。
    さらに、 DTSTARTプロパティが現地時間の日付として指定されている場合、UNTILルール部分も現地時間の日付として指定されなければならない
  • DTSTARTプロパティがUTC時間付きの日付または現地時間とタイムゾーン参照付きの日付として指定されている場合、UNTILルール部分はUTC時間付きの日付として指定されなければならない
  • STANDARDDAYLIGHTサブコンポーネントの場合、UNTILルールパートは常にUTC時間付きの日付として指定されなければならない
  • DATE-TIME値として指定されているなら、それはUTC時間フォーマットで指定されなければならない
  • UNTIL が存在しない場合、およびCOUNTルール部分も存在しない場合、RRULE は永遠に繰り返されると見なされる。

COUNT

  • 出現回数を定義
  • DTSTARTの値は最初の出現としてカウントされる

BYSECOND

  • 1分以内の秒。カンマ区切りのリストで表される
  • 有効な値は0から60
  • DTSTARTDATE の場合は指定できない

BYMINUTE

  • 1時間以内の分。カンマ区切りのリストで表される
  • 有効な値は 0 - 59
  • DTSTARTDATE の場合は指定できない

BYHOUR

  • 1日の時間。カンマ区切りのリストで表される
  • 有効な値は 0 - 23
  • DTSTARTDATE の場合は指定できない

BYDAY

  • カンマ区切りの曜日のリストを表す
  • FREQ=MONTHLY ルールでは +1MO はその月の最初の月曜日を意味し、 -1MO は最後の月曜を意味する
  • FREQ=YEARLY の場合、BYMONTH で指定した月のオフセットに対応し、 BYWEEKNO または BYMONTH が存在する年内のオフセットに対応する。 (注:仕様書上BYMONTHが2回出てくるので文章が間違ってるかも)
  • 数値が指定されていない場合、毎週実行される
  • FREQ=MONTHLYまたはYEARLYでない場合は数値は指定できない
  • FREQ=YEARLYBYWEEKNO が指定されている場合は BYDAY に数値は指定できない

BYMONTHDAY

  • カンマ区切りの月の日のリストを表す
  • 有効な値は1から31または-31から-1
  • -10はその月の最後の日から10日を表す
  • FREQ=WEEKLY の場合には指定できない

BYYEARDAY

  • カンマ区切りの年の通算日を表す
  • 有効な値は1から366または-366から-1
  • -1は年の最後の日(12月31日)を表す
  • -306は、その年の最後の日から306日前(3月1日)を表す
  • FREQDAILY,WEEKLY, MONTHLYのいずれかの場合は指定できない

BYWEEKNO

  • カンマ区切りの年の何番目の週かを表す
  • 有効値は1〜53 または-53から-1
  • 週はWKSTで定義された曜日から始まる7日間を表す
  • 暦年の週番号1は、その暦年の最低4日を含む最初の週
  • FREQ=YEARLY 以外では指定できない
  • BYWEEKNO=3 は年の3番目の週を表す

BYMONTH

  • カンマ区切りの年の月を表す
  • 有効値は1〜12

WKST

  • 週の始まりの曜日を指定する(MO, TU, WE, TH, FR, SA, and SU)
  • デフォルトは月曜

BYSETPOS

  • ルールで指定された繰り返しのn番目の発生に対応する値をカンマ区切りで指定する
  • 1つのINTERVAL 内の一連の繰り返しに対して処理する
  • 例えば FREQ=WEEKLYの場合、毎週の先頭に対して作用する
  • 有効な値は1から366または-366から-1
  • BYxxxと共に使用される必要がある
  • もし指定された場合、特定のルールの条件を満たすもののなかでn番目に発生するものを示す

例:月の最終就業日

FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1

その他

  • 繰り返しルールは無効な繰り返しインスタンスを生成する可能性がある
  • それらは無視し、ルールとしてカウント対象として認めない。
  • ルールに含まれない情報はDTSTART から派生(継承)する
  • FREQ=YEARLY;BYMONTH=1 は月の何日かを指定していないが、DTSTARTの日が使われる
  • BYxxxは何らかの方法で繰り返し回数を修正する。
  • frequencyより大きいBYxxxの指定は通常発生回数を減らす
    • FREQ = DAILY; BYMONTH = 1 は毎日→1月のみになる
  • frequencyより小さいBYxxxの指定は通常発生回数を増やす
  • FREQ = YEARLY; BYMONTH = 1,2 は 毎年1回から2回になる

評価順について

  • 複数のBYxxxが指定されている場合、FREQINTERVALが評価されたあとで以下の順に適用される
    1. BYMONTH
    2. BYWEEKNO
    3. BYYEARDAY
    4. BYMONTHDAY
    5. BYDAY
    6. BYHOUR
    7. BYMINUTE
    8. BYSECOND
    9. BYSETPOS
    10. COUNT / UNTIL

依存関係

表の意味

  • N/A はそのFREQでは使用できない
  • BYDAYFREQに応じて特殊な振る舞いをする(Note1, 2を参照)
  • LimitはFREQの対象が狭まる
  • ExpandはFREQの対象が広がる
SECONDLY MINUTELY HOURLY DAILY WEEKLY MONTHLY YEARLY
BYMONTH Limit Limit Limit Limit Limit Limit |Expand
BYWEEKNO N/A N/A N/A N/A N/A N/A |Expand
BYYEARDAY Limit Limit Limit N/A N/A N/A |Expand
BYMONTHDAY Limit Limit Limit Limit N/A Expand |Expand
BYDAY Limit Limit Limit Limit Expand Note 1 |Note 2
BYHOUR Limit Limit Limit Expand Expand Expand |Expand
BYMINUTE Limit Limit Expand Expand Expand Expand |Expand
BYSECOND Limit Expand Expand Expand Expand Expand |Expand
BYSETPOS Limit Limit Limit Limit Limit Limit |Limit
  • Note 1. BYMONTHDAYがある場合は制限される。そうでなければMONTHLY用にExpandされる
  • Note 2. BYYEARDAY or BYMONTHDAY がある場合は制限される。
    • そうでなくBYWEEKNO がある場合は WEEKLY が拡張される
    • そうでなくBYMONTH がある場合は MONTHLY が拡張される
    • そうでない場合は YEARLYが拡張される

参考

/* Responsive: yes */