とーますメモ

Ruby on Rails / Goなどの学習メモ

【Rails】migrationファイル実行時に同時にSQLを実行する方法

作成するmigrationファイルの処理は、1つのカラムを追加するというシンプルな処理のみを行う。
しかしdefault値は設定せず、全レコード上の作成されるカラムには、特定の値を埋め込みたいという要件があったため
migration実行時にカラムを追加後、その特定値を埋め込む処理も同時に入れたくなった。

以下のページが参考になった。
qiita.com

class ChangeTests < ActiveRecord::Migration
  def change
    add_column :sample_tables, :test_column, :string, :after => :previous_column

    begin
      ActiveRecord::Base.connection.execute("START TRANSACTION")

      ActiveRecord::Base.connection.execute("
        UPDATE
          sample_tables
        SET
          new_column = 'default_value'
        WHERE
          new_column IS NULL;
      ")

      ActiveRecord::Base.connection.execute("COMMIT")
    rescue => e
      ActiveRecord::Base.connection.execute("ROLLBACK")
      raise "Error: #{e.to_s}"
    end
  end
end

【MySQL】フランス語などのアクセントが正しく取得できない。

以下が参考になった。

mysql – selectクエリを使用してアクセント記号のない文字でカラムをフィルタリングする方法 - コードログ
utf 8 - How to conduct an Accent Sensitive search in MySql - Stack Overflow

以下のように「COLLATE utf8_bin」を付ければ良い。

WHERE col_name = 'abád' COLLATE utf8_bin

【Rails】ActiveRecordの属性メソッドを上書きする

使用しているWebアプリの1つに
Railsが管理しているDBではない、他のDBからのデータを取得する必要があった。

当たり前の話だが、Railsではapplication.rbのtime_zoneの設定のため
日付取得の際に自動で、タイムゾーン変換が行われる。

thoames.hatenadiary.jp

そのため、以下の状況になってしまった。

①Railsが管理しているDBの日付は、自動タイムゾーン変換をしてほしい。
②Rails外のDBの日付は、自動タイムゾーン変換をしてほしくない。(*このDBの日付はDATETIME型)

前提として、このWebアプリではconfig.time_zoneは指定しているが、config.active_record.default_timezoneは指定していない。
その理由は以下の記事で詳しく解説されている。
Rails と時刻 - @kyanny's blog

この対応策としてタイトルにもある通り
Rails外のDBテーブルを管理するActiveRecordモデルの属性メソッドを上書きする」という方法で対応することにした。

その方法としては以下の記事が役に立った。

ActiveRecordのattribute methodをオーバーライドするとき
Active Recordで属性にアクセスするメソッドを上書きする - 飲んだり寝たり

読み込み専用の「read_attribute」のaliasである[]を利用することで、属性メソッド呼び出しの際に発生してしまうループ状況を回避し上書きすることができるようになる。
Railsの設定で「config.active_record.default_timezone」は設定していないのでRailsはDBの日付をUTCとして解釈し保存する。
これで元の日付データを読み込んだ後(この時点で日付は既にタイムゾーン変換が行われている)、UTCに戻してあげれば変換前の日付を取得できる。

UTCタイムゾーンの日付に変更する方法としては以下の方法があるが、それぞれ違いがある
1)to_s(:db)・・・返り値がString型 ex) self[:xxx_date].to_s(:db)
2)utc・・・返り値がTime型 ex) self[:xxx_date].utc
3)in_time_zone('UTC')・・・返り値がActiveSupport::TimeWithZone型 ex) self[:xxx_date].in_time_zone('UTC')

1)の方法を使用した場合、String型として返されるので、後ほど「strftime("%d/%m/%Y")」などを使用しフォーマットを変更したい場合などに
融通が効かないので不採用。問題は2)か3)なのだが、Time型とTimeWithZone型は互換性があるため、どちらにするか迷ったが
この記事(RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita)でも紹介されている通り、自分も極力TimeWithZone型を使用する方針で
行きたいのでここは3)を使用することにした。
※3)は内部で2)を行い、TimeWithZone型にラップしているっぽい。コードの詳細は以下。
rails/zones.rb at 98a57aa5f610bc66af31af409c72173cdeeb3c9e · rails/rails · GitHub


そして最終的に作成したコードは以下。このコードではxxx_dateがActiveRecordの属性メソッド。

class Hoge < ActiveRecord::Base

  establish_connection "xxxxxx".to_sym
  self.table_name = 'yyyyyy'

  def xxx_date
    self[:xxx_date].in_time_zone('UTC')
  end
end

長かった...

[参考]
TimeとTimeWithZoneの違いについて - 研鑽の日々
ruby - Rails 4 - in_time_zone unexpected behavior - Stack Overflow
When using time zones, beginning_of_day / end_of_day is broken in Rails 2 for any Date or DateTime - makandra dev

【Rails】難しすぎる!? Timezone(タイムゾーン)についてのメモ

自分用メモ。

大事なことは全て以下のページにあった。
qiita.com
qiita.com

上記の内容から重要なポイントを抜き出すと以下の通り。

1)タイムゾーンの設定

システムまたは環境変数のタイムゾーンとapplication.rb(time_zone)が同じならば、
Time.nowでもTime.current(Time.zone.now)でも同じ。

2)Rails経由でDBに保存されるtimeについて

[config.active_record.default_timezon]を[:local]に設定していなければ、RailsはMySQLのタイムゾーンを無視して時間をUTCで保存する。

3)Datetime.newはUTCタイムゾーンを返す。

parse や new では、環境変数のタイムゾーンでもapplication.rbのタイムゾーンでもなく、UTCのタイムゾーンになる点も注意が必要です。(UTCからのオフセットを指定しない場合)

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い - Qiita

4)MySQLのDateTime型はタイムゾーンを情報を持っていない。

Railsと周辺のTimeZone設定を整理する (active_record.default_timezoneの罠) - Qiita

5)MySQLのTIMESTAMP型は厄介?

TIMESTAMP型は書き込む時にサーバのタイムゾーン設定を確認してUTCに変換してからデータを保存する。取得する時はUTCからサーバのタイムゾーンに変換してから表示する。
なので、記録時と異なるタイムゾーンに変更すると取得した時の時間がズレてしまう。

by Railsと周辺のTimeZone設定を整理する (active_record.default_timezoneの罠) - Qiita

またTIMESTAMP型は「2038年問題」などの問題もあるため
将来的なことを考えた場合、時間を保持する型としては「Datetime型」一択だと思う。(個人的見解)
Railsのcreated_atやupdated_atもDatetime型だし、migrationにtimestampを選択できるけど、rake db:migrateしたらDatetime型になってるし。

他にも「CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP」のせいで、レコードが更新されると勝手に日付が更新されてしまうといった仕様ですし。

MySQLの timestamp型が、なかなか厄介。
MySQL TIMESTAMP 2つ以上のCURRENT_TIMESTAMP - Oboe吹きプログラマの黙示録

結論

上記の記事で言ってることと同じだが
Time.nowやDateTime.nowなどの環境タイムゾーンを使用している関数を使うよりも
Time.currentなどのapplication.rbで使用しているタイムゾーンを元にした関数を使うほうが良い。

備考

DateTime型のカラムに対して、Date型で検索をかけるときは十分注意すること。

リファレンスマニュアルに書かれているとおりで、最近のMySQLではDateTime型のカラムに対してDate型で検索をしようとすると自動的に '00:00:00' が付与されてDateTime型として計算されます。

by MySQLでDateTime型のカラムをDate型で検索するときに気をつけること - (゚∀゚)o彡 sasata299's blog

【Rails】サービスクラスについて自分なりにまとめてみた

最近、設計した画面が4〜5個のモデルが絡む画面があった。
その時コントローラー内で実装した場合、肥大化することが容易に想像でき、
またどれか一つのモデル内で実装する処理としては複雑なため「サービスクラス」が必要なのではないかと思い調べてみた。

いろいろ調べているとこのページの内容がすごい良かった。
肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

自分のケースの処理だと、入出力とバリデーションに対しての処理がメインになるため
この記事内で言う「Form Object」のパターンで対応することにした。
他にも「ActiveRecord」のオブジェクトを"装飾"して使用するDecoratorパターンは今後使えそう。

しかし、今後サービスクラス(Service Object)のケースを使用するケースが
あるかもしれないのでこの機会に詳細を調べてみる。
色々と検索すると、「サービスクラス」を安易に使用すると、逆にカオスになるので
使用は注意したほうが良いという記事が沢山見つかった。

サービスクラスを使用しようと思うケースは以下のパターンがあると思う。

1)肥大化したモデルのメソッドをサービスクラスに移す

本来モデルが持つべき知識を移しているので、サービスクラス内のコードが再利用しにくい。
この場合、「Value Object」や、同じグループのロジックを切り出したバニラクラスを継承や「コンポジション(または集約)」として使用するケースを
一度考えてみる。そのためにRailsにはdelegateやcomposed_ofなどの便利な機能がある。
単一モデルが肥大化した場合は、サービスクラスは使用しない方が良いと思う。

Railsアンチパターン&lt;モデル編&gt;②Fat model - Qiita
継承のMix-inについて - Kazunori Kamiya - Medium
俺が悪かった。素直に間違いを認めるから、もうサービスクラスとか作るのは止めてくれ - Qiita

2)複数モデルを触るのでサービスクラスを作る

まずは本来モデルが持つべき責務ではないか?、誰が担う責務なのかを考える。
なぜならサービスクラスの責務を強制する仕組みがないため、実装者によっては、まったく意図しないコードができる可能性があるため。
そのため本来モデルにあるべきコードがサービスクラスに実装されてしまう状態が発生する。

ここでサービスクラス内ではなく、モデルクラス内にビジネスロジックを書けばよいのでは?という疑問もあると思うが
自分の場合、サービスクラスを使用する判断としては、「モデルが3つ以上絡む」場合に使用する方針にする。
それモデルが2つ程度で、リレーションがあるモデルなら、責務範囲として、いずれかのモデル内で完結させる。

また独自の計算ロジック、認証、外部API連携、プッシュ通知、メール送信など、「どのモデルにも属さないロジック」がある場合は、
サービスクラス内で書く。

Form Objectとの使い分けは以下

Form Object: 入出力がメインな処理
Serivce Object: ビジネスロジック

サービスクラスを使用する場合は、以下のルールに従って作成

クラス名: #{動詞}#{目的語}Service(#{名詞}#{動詞}er/orという命名方法もあるが、自分は誰が見ても、役割がすぐに分かるServiceを名前に入れる)
メソッド: publichメソッドは、execute(callやrunもあるが、自分はexecuteが好み)の1つのみ。他はprivateメソッドにしてカプセル化する。(単一責任の原則)
配置場所: app/services/内
責務: 1つのビジネスロジックのみ。

Service Objectでありがちなアンチパターンとしては、たとえばManageUserというserviceがユーザーの作成と削除の両方を引き受けてしまうというものがあります。そもそもmanageという言葉だけでは責務があいまいになってしまい、オブジェクトがどんなアクションを実行すべきかを制御する方法も明確になりません。代わりにDeleteUserとCreateUserという2つのserviceに分けて導入すれば、コードも読みやすくなり、より自然になります。

Railsで重要なパターンpart 1: Service Object(翻訳)

以下のモジュールをincludeすることでXXXXService.new.call(params)と呼び出さず、XXXXService.call(params)と呼び出すことができる。
またexecuteの戻り値はサービスクラスのインスタンスを返す。
※以下のモジュールの読み込みもとにもexecuteのインスタンスメソッドが必要

module Service
  extend ActiveSupport::Concern
  class_methods do
    def execute(*args)
      service = new(*args)
   service.execute
      service
    end
  end
end

[参考]
RailsのService層とうまく付き合うにはどうすればいいのか調べてみた - 男女比はカレーと福神漬けと同じくらい
Railsで重要なパターンpart 1: Service Object(翻訳)
Railsプロジェクトにサービスクラスを導入して4ヶ月たったので振り返る - Qiita
Railsでやってしまいがちな保守性を下げてしまうコードとその解決策 - Qiita
Ruby (off|with) the Rails - Speaker Deck

追記

エラー設計の良記事:
blogs.msdn.microsoft.com

【Rails】Rails4で親モデル用のサブディレクトリを作成し、STI(単一テーブル継承)をやってみる

サブディレクトリの作成

サブディレクトリを配置する場所は
app/models内。

ディレクトリ名はここでは「app/models/parents」とした。

Rails4ではmodels直下しか読み込まれないので
「config/application.rb」内に以下を追加し、models内のサブディレクトリ内のファイルも読み込まれるようにする。

config.autoload_paths += Dir["#{config.root}/app/models/**/"]

STIの設定

STIの説明については、以下のページが詳しい。
Railsでスーパータイプ/サブタイプを表現する方法を比較してみる - Qiita
[Rails]Model/テーブル設計で必ず覚えておきたいSTI | Raccoon Tech Blog [株式会社ラクーンホールディングス 技術戦略部ブログ]
みんなRailsのSTIを誤解してないか!? - Qiita

STIではなく具象クラスで対応する場合は以下の方法も使えそう。
[Rails 5] モデルの継承元がActiveRecord::BaseからApplicationRecordに変更された
[Rails] self.abstract_class = true の意味と挙動 « Codaholic

まずはモデルが参照するテーブルにtypeカラムを追加する。
XXXXXはテーブル名

$ bundle exec rails g migration AddTypeToXXXXX type:string

このtypeには「サブクラス名」が保存される。
サブクラスでnewすると、そのサブクラス名が自動的に保存され
allやfindすると自動的に、条件文にtype IN ('サブクラス名')が指定される。

すごい便利。

[その他参考]
サブディレクトリに配置したモデルクラスの名前を変更する - Ruby1.9とRails3で何か作ってみる
モデルをサブディレクトリに分割する Rails 3.2 - Qiita
https://qiita.com/yebihara/items/9ecb838893ad99be0561