Rspecで配列の値を検証するcontain_exactly

こんにちは!kossyです!




さて、今回はRspecで配列の値を検証するcontain_exactlyの使い方を
ブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.3.2
rspec-rails 4.0.1



使い方

前提として、TenantモデルとUserモデル(自己参照付き)があるとして、以下のscopeがUserモデルに定義されてあるとします。

  scope :tenant_managers, -> (tenant_id) {
    where(tenant_id: tenant_id).where(manager_id: nil)
  }

このスコープのテストとして、

  describe 'scopes' do
    describe 'tenant_managers -> (tenant_id)' do
      it '' do
        tenant = create :tenant
        manager_1 = create :user, tenant: tenant
        manager_2 = create :user, tenant: tenant
        user = create :user, tenant: tenant, manager: manager_1

        expect(User.tenant_managers(tenant.id)).to include manager_1
        expect(User.tenant_managers(tenant.id)).to include manager_2
        expect(User.tenant_managers(tenant.id)).to_not include user
      end
    end
  end

とするテストケースもかけますが、contain_exactyマッチャを使うことで、
より簡潔に記述することができます。

  describe 'scopes' do
    describe 'tenant_managers -> (tenant_id)' do
      it '' do
        tenant = create :tenant
        manager_1 = create :user, tenant: tenant
        manager_2 = create :user, tenant: tenant
        user = create :user, tenant: tenant, manager: manager_1

        expect(User.tenant_managers(tenant.id)).to contain_exactly(manager_1, manager_2)
      end
    end
  end

contain_exactlyの引数に検証したい値を指定することで、動作の検証を行うことができます。




勉強になりました。

Angular CLI 11 で ng new してみた

こんにちは!kossyです!



さて、今回はAngular11が最近リリースされたとのことで、
早速ng newコマンドを試して、何が変わったのか検証してみたいと思います。




環境
node 12.13.1
npm 6.14.8
Angular 11.0.1



コマンド実行!

$ ng new sample

? Do you want to enforce stricter type checking and stricter bundle budgets in the workspace?
  This setting helps improve maintainability and catch bugs ahead of time.
  For more information, see https://angular.io/strict

https://angular.io/strict アクセスしたら https://angular.io/guide/strict-mode にリダイレクトした、、、

上記ドキュメントにも書いてあるとおり、Strict Modeを有効化するための設定ですね。

? Would you like to add Angular routing?

これは前のバージョンからある設問ですね。
ルーティング機能が必要なのであればyを押してenterしましょう

? Which stylesheet format would you like to use? (Use arrow keys)
❯ CSS
  SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
  Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
  Less   [ http://lesscss.org                                             ]
  Stylus [ https://stylus-lang.com                                        ]

CSSの言語はどれにします?っていう設問ですね。
これも前のバージョンからありました。



うーん変わったのは一番最初の設問だけかな?


tsconfig.jsonの違い

1番目の設問で、strictモードにしなかった時としたときの差分をみてみました。

true

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}

false

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  }
}

ここではtsconfig.jsonの詳しい設定には触れませんが、
no...の部分と、AngularCompilerOptionが差分になっていますね。
この部分については別記事で触れたいと思います。



Readmeも比較してみた

これはあんまりかわったところはなさそう
f:id:kossy-web-engineer:20201118231339p:plain



勉強になりました。

Angular 11 へのアップデート時の 'name' is defined as an accessor in class 'CdkColumnDef' エラーの対処法

こんにちは!kossyです!




さて、今回はAngular 9のアプリケーションを11にアップデートした際に発生した、
'name' is defined as an accessor in class 'CdkColumnDef' エラーの対処法について、
ブログに残してみたいと思います。




環境
node 12.13.1
npm 6.14.8
Angular 9.1.7 => 11.0.1



AngularMaterialとAngularCDKをアプデする

以下のサイトで、「@angular/materialと@angular/cdkをuninstallして、最新版をインストールしなよ!」という
単純明快な答えに言及しておりました。

$ npm remove @angular/material @angular/cdk

$ npm i @angular/material@latest @angular/cdk@latest

これでng serve 時にエラーが発生しなくなりました。
Angularはng updateするだけで(大体)問題なくアップデートできていいですね、、、

graphql-rubyのGetting Startedを翻訳してみる

こんにちは!kossyです!




さて、今回はGraphQLの勉強がてら、graphql-rubyのreadmeを翻訳してみようと思います。(こうでもしないと真面目に読まない気がして、、、)
Getting Startedまでの翻訳を頑張ってみます。



偉大なる本家様リポジトリはこちら

Getting Started

インストール

あなたのアプリケーションのGemfileに追加することで、RubyGemsからgraphqlをインストールできます。

./Gemfile

gem 'graphql'

bundle install を 忘れずに。

$ bundle install

はじめに

Railsでは、いくつかのGraphQLジェネレーターを使い始めることができます。

# graphql-rubyのボイラープレートを追加し、開発環境にgraphiqlをマウントします

$ rails g graphql:install



# 最初のオブジェクトタイプを作成します

$ rails g graphql:object Post title:String rating:Int comments:[Comment]

または、以下のようなGraphQLサーバーを手動で構築することもできます。

・いくつかのタイプを定義する
・それらをスキーマに接続する
スキーマを使用してクエリを実行する

タイプ宣言

タイプはアプリケーション内のオブジェクトを記述し、GraphQLのタイプシステムの基礎を形成します。

# app/graphql/types/post_type.rb

module Types
  class PostType < Types::BaseObject
    description "A blog post"
    field :id, ID, null: false
    field :title, String, null: false

    # フィールドはキャメルケースで問い合わせる必要があります(これは `truncatedPreview`になります)
    field :truncated_preview, String, null: false

    # フィールドは他のオブジェクトのリストを返すことができます
    field :comments, [Types::CommentType], null: true,

      # また、フィールドには独自の説明を付けることができます。
      description: "This post's comments, or null if this post has comments disabled."
  end
end

# app/graphql/types/comment_type.rb

module Types
  class CommentType < Types::BaseObject
    field :id, ID, null: false
    field :post, PostType, null: false
  end
end

スキーマを構築する

スキーマを構築する前に、システムへのエントリポイントである「クエリルート」を定義する必要があります。

class QueryType < GraphQL::Schema::Object
  description "The query root of this schema"

  # 最初にフィールドの署名について記述します。
  field :post, PostType, null: true do
    description "Find a post by ID"
    argument :id, ID, required: true
  end

  # 次に、実装を行います。
  def post(id:)
    Post.find(id)
  end
end

次に、QueryTypeをクエリエントリポイントとして使用してスキーマを構築します。

class Schema < GraphQL::Schema
  query QueryType
end

これでこのスキーマはGraphQLクエリを提供する準備ができています!ガイドを参照して、他のGraphQLRuby機能について学習してください。


クエリを実行する

クエリ文字列からクエリを実行できます。

query_string = "
{
  post(id: 1) {
    id
    title
    truncatedPreview
  }
}"
result_hash = Schema.execute(query_string)
# {
#   "data" => {
#     "post" => {
#        "id" => 1,
#        "title" => "GraphQL is nice"
#        "truncatedPreview" => "GraphQL is..."
#     }
#   }
# }

スキーマでクエリを実行する方法の詳細については、クエリの実行を参照してください。

Relayを使う

Relayのバックエンドを構築する場合は、次のものが必要です。

GraphQL :: Introspection :: INTROSPECTION_QUERYを送信することで取得できるスキーマJSONダンプ
・GraphQLのRelay固有のヘルパー。GraphQL:: Relayガイドを参照してください。

Appollo Clientを使う

Apollo Clientは、人気のあるビューレイヤーに便利に統合された、フル機能の使いやすいGraphQLクライアントです。
Apolloクライアントをgraphql-rubyサーバーに接続するために特別なことをする必要はありません。

GraphQL.jsクライアントを使う

GraphQL.jsクライアントは、プラットフォームやフレームワークに依存しない小さなクライアントです。
GraphQLリクエストはHTTPを介して転送される単純なクエリ文字列であるため、graphql-rubyサーバーでうまく機能します。

RailsのGem devise のvalidatableのソースコードを読んでみた

こんにちは!kossyです。



さて、今回はRailsプロジェクトにおいて認証機能を作成する時に用いられるGemであるdeviseの、
validatableのソースコードを読んでみたので、ブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.3
devise



まずはドキュメントを読む

上記ページのドキュメントを読んでみました。
Google翻訳先生に頼りつつ、一部意訳します。

Validatable creates all needed validations for a user email and password.
It's optional, given you may want to create the validations by yourself.
Automatically validate if the email is present, unique and its format is valid.
Also tests presence of password, confirmation and length.

Validatableは、ユーザーの電子メールとパスワードに必要なすべての検証を行います。
これはオプショナルです、なので自分で検証を行いたい場合は、自分で実装してください。

電子メールが存在し、一意であり、その形式が有効であるかどうかを自動的に検証します。
また、パスワードの存在、確認、および長さをテストします。

出典: https://rubydoc.info/github/plataformatec/devise/master/Devise/Models/Validatable

uniqueチェックや、presenceチェック、regexpチェックも行ってくれる優れものですね。

では中ではどのようにしてチェックを行っているのでしょうか。次の章でコードを読んでみるとします。


ソースコードを追う(その1)

以下のコードをご覧ください。

VALIDATIONS = [:validates_presence_of, :validates_uniqueness_of, :validates_format_of,
                     :validates_confirmation_of, :validates_length_of].freeze

validationを行う要素について、定数化を行っている箇所ですね。
先ほどのドキュメント翻訳にも記載されている通り、
存在確認・一意性確認・フォーマット確認・パスワード存在確認・長さ確認
を行うようです。

次は以下のメソッドをみてみます。

      def self.included(base)
        base.extend ClassMethods
        assert_validations_api!(base)

        base.class_eval do
          validates_presence_of   :email, if: :email_required?
          if Devise.activerecord51?
            validates_uniqueness_of :email, allow_blank: true, case_sensitive: true, if: :will_save_change_to_email?
            validates_format_of     :email, with: email_regexp, allow_blank: true, if: :will_save_change_to_email?
          else
            validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
            validates_format_of     :email, with: email_regexp, allow_blank: true, if: :email_changed?
          end

          validates_presence_of     :password, if: :password_required?
          validates_confirmation_of :password, if: :password_required?
          validates_length_of       :password, within: password_length, allow_blank: true
        end
      end

ClassMethodsをextendしていますが、中身をみてみましょう。

      module ClassMethods
        Devise::Models.config(self, :email_regexp, :password_length)
      end

こちらですね。

Railsでdeivseを使う時に、config/initializer配下にdeviseのinitファイルが追加されますが、
その中に、email_regexpとpassword_lengthに関する記述があります。

  # ==> Configuration for :validatable
  # Range for password length.
  config.password_length = 6..128

  # Email regex used to validate email formats. It simply asserts that
  # one (and only one) @ exists in the given string. This is mainly
  # to give user feedback and not to assert the e-mail validity.
  config.email_regexp = /\A[^@\s]+@[^@\s]+\z/

このconfigで設定した値が、実際にバリデーションに使われる値になります。

デフォルトではパスワードの文字数は6文字から128文字の間で、
メールアドレスは簡易な正規表現でのチェックになっていますね。

この正規表現だと、通常メールアドレスでは使われない { や [ も使えてしまいますし、
Rails公式のガイドに記載された、

/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i

https://guides.rubyonrails.org/active_record_validations.html#common-validation-options

を使うのがいいのかなと思いますが、、、

簡易な正規表現に落ち着いた経緯については、以下のブログが詳しかったです。



少し話が逸れましたね、、、
コードに話を戻します。

もう一度、 def self.included メソッドをみてみましょう。

      def self.included(base)
        base.extend ClassMethods
        assert_validations_api!(base)

        base.class_eval do
          validates_presence_of   :email, if: :email_required?
          if Devise.activerecord51?
            validates_uniqueness_of :email, allow_blank: true, case_sensitive: true, if: :will_save_change_to_email?
            validates_format_of     :email, with: email_regexp, allow_blank: true, if: :will_save_change_to_email?
          else
            validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
            validates_format_of     :email, with: email_regexp, allow_blank: true, if: :email_changed?
          end

          validates_presence_of     :password, if: :password_required?
          validates_confirmation_of :password, if: :password_required?
          validates_length_of       :password, within: password_length, allow_blank: true
        end
      end

class_evalメソッドを使って、引数のbaseに動的にvalidationを定義しています。

ActiveRecord::Validations::PresenceValidatorに定義されている、validates_presence_of を emailに対して適用しています。

email_required?は、

      def email_required?
        true
      end

有無を言わさず true を返すようになっていました。笑

if Devise.activerecord51? はどんな処理でしょうか。

  def self.activerecord51? # :nodoc:
    defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new("5.1.x")
  end

みた感じ、Gemのバージョンが5.1.x より大きい時とそうでない時を判断するためのメソッドみたいですね。
5.1系から、attr_changed?等のメソッドが非推奨になったのに合わせて、Gemのバージョンで分岐させるようにしたみたいですね。

Gemの中身を見ていると、こういう泥臭い実装が出てきて面白いですね。

続きのvalidationオプションの使い方は、

を見てみてください。




ソースコードを追う(その2)

assert_validations_api!(base) メソッドをみるのを忘れてました。

      def self.assert_validations_api!(base) #:nodoc:
        unavailable_validations = VALIDATIONS.select { |v| !base.respond_to?(v) }

        unless unavailable_validations.empty?
          raise "Could not use :validatable module since #{base} does not respond " <<
                "to the following methods: #{unavailable_validations.to_sentence}."
        end
      end

先ほど記載したvalidations定数に対して、selectメソッドを用いて、「baseにvalidationsのメソッドが定義されていないもの」を集めて返した後、
それが一つでもあった場合、validatableモジュールを使えないように、例外を投げています。

password_required?はどんな処理でしょうか。

      # Checks whether a password is needed or not. For validations only. パスワードが必要かどうかを確認します。検証のみ。
      # Passwords are always required if it's a new record, or if the password 
      # or confirmation are being set somewhere.
      # 新しいレコードの場合は、常にパスワードが必要です。
      # または、パスワードまたはパスワード確認がどこかに設定されている場合。

      def password_required?
        !persisted? || !password.nil? || !password_confirmation.nil?
      end

新しいレコードの場合かパスワード(確認用も)が入力されていない場合にtrueが返るメソッドですね。


これで一通り処理を追えたかと思います。


まとめ

思ったよりも難しいことはしていない印象です。

正規表現がゆるゆるなのは驚きました。
悪戯で変なメールアドレスが登録されるのも不具合の元になりますので、
deviseを導入する際には、デフォルトの正規表現を変更するのは必須かもしれませんね。

勉強になりました。



RailsのRspecでランダムに落ちるテストを再現したい

こんにちは!kossyです!




さて、今回はRailsでテストツールにRspecを採用している際に、
ランダムに落ちるテストをseed値を指定することでもう一度再現する方法について、
ブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.3
MacOS catalina



設定

seed値を設定してRspecを実行するために、spec/spec_helper.rbを編集します。

  config.order = :random # ファイルの上から順番に実行するのではなくランダムな順序で実行できるようにする
  Kernel.srand config.seed # seed値を指定してテストを実行できるようにする

上記の設定を行うと、

Finished in 0.32274 seconds (files took 7.83 seconds to load)
4 examples, 0 failures

Randomized with seed 40251

のように、テストの実行が終了するとseed値が表示されるようになります。

$ bundle exec rspec --seed 40251
    • seedオプションにseed値を指定することで、同条件で実行できます。


勉強になりました。

Railsのcreated_atがどのようにして自動で保存されているのか調べてみた

こんにちは!kossyです!




さて、今回はRailsのcreated_atカラムにどのようにして値が自動で保存されるかについて、
ブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.3
MacOS catalina



まずはドキュメントを探す

自分のググり方が悪いのか、解説している記事がなかなか見つからなかったので、
ソースコードを読んでみることにしました。

rails created_at 自動 と検索して、たぶん下記のファイルでcreated_atやupdated_atに関する処理を行っているとみました。

まず、Google翻訳大先生にコメントアウト部分を訳してもらいます。一部よしなに変更して訳しました。

ActiveRecord automatically timestamps create and update operations
if the table has fields named 「created_at/created_on」or「updated_at/updated_on」.
  
ActiveRecordはレコードの作成または更新時にそのテーブルに
「created_at/created_on」か「updated_at/updated_on」という名前のフィールドがあれば、自動的にタイムスタンプを付与します。
  
Timestamping can be turned off by setting:
  
タイムスタンプ機能をオフにしたい場合は、config/application.rbで以下の記述をします。
  
config.active_record.record_timestamps = false
  
Timestamps are in UTC by default but you can use the local timezone by setting:
  
タイムスタンプはデフォルトでUTCですが、config/application.rbでローカルタイムゾーンを使用できます。
  
config.active_record.default_timezone = :local
  
== Time Zone aware attributes
  
タイムゾーン対応の属性
  
ActiveRecord keeps all the「datetime」and「time」columns timezone aware.
By default, these values are stored in the database as UTC
and converted back to the current「Time.zone」when pulled from the database.
  
Active Recordは、すべての「datetime」列と「time 」列のタイムゾーンを認識し続けます。
デフォルトでは、これらの値はUTCとしてデータベースに保存されます。
データベースからプルすると、現在の「Time.zone」に変換されます。
  
This feature can be turned off completely by setting:
  
この機能は、以下をconfig/application.rbで設定することで完全にオフにできます。
  
config.active_record.time_zone_aware_attributes = false
  
You can also specify that only「datetime」columns should be time-zone
aware (while「time」should not) by setting:
  
次のように設定することで、日時列のみがタイムゾーンを認識するように指定することもできます(時間は認識されません)。
  
ActiveRecord::Base.time_zone_aware_types = [:datetime]
  
You can also add database specific timezone aware types. For example, for PostgreSQL:
  
データベース固有のタイムゾーン対応タイプを追加することもできます。
たとえば、PostgreSQLの場合:
  
ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange]
  
Finally, you can indicate specific attributes of a model for which time zone conversion should not applied, for instance by setting:
  
最後に、たとえば次のように設定することで、タイムゾーン変換を適用してはならないモデルの特定の属性を指定できます。
  
class Topic < ActiveRecord::Base
self.skip_time_zone_conversion_for_attributes = [:written_on]
end
  
出典: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/timestamp.rb

時刻周りは意外と柔軟に設定できるインターフェイスになっている印象を受けました。
各オプションを活用するユースケースはパッと思いつきませんが、、、(詳しい方、教えてください)


ソースコードを追う(その1)

コメントアウト部分の意図を汲み取ったところで、次は本丸のコードリーディングです。

目についたのは77行目のcurrent_time_from_proper_timezoneメソッドです。

def current_time_from_proper_timezone
  default_timezone == :utc ? Time.now.utc : Time.now
end

このメソッドは、ActiveRecord::Timestamp::ClassMethodsに定義されていて、
ActiveRecord::Baseにincludeされているため、ActiveRecord::Baseを継承したモデルから呼び出せます。

以下のファイルの299行目でincludeしています。

Moduleに定義したinstance_methodsは外部からincludeすれば使えるようになりますゆえ。

ではコンソールで試してみましょう。
前提として、ActiveRecord::Baseを継承したUserモデルが定義してあるとします。

User.current_time_from_proper_timezone
=> 2020-11-07 09:28:28 +0900

はい、返り値としてTimeクラスのインスタンスが返って来ました。

config/application.rbで、

    config.active_record.default_timezone = :local

を指定しているので、Time.nowの返り値になっています。

このcurrent_time_from_proper_timezoneの値をcreated_atおよびupdated_atに保存していると思われます。



もっと深く追ってみましょう。


ソースコードを追う(その2)

current_time_from_proper_timezoneはどこから呼び出されているでしょうか。

いくつか呼び出し箇所がありましたが、

ここから呼び出されています。

      def touch_attributes_with_time(*names, time: nil)
        attribute_names = timestamp_attributes_for_update_in_model
        attribute_names |= names.map(&:to_s)
        attribute_names.index_with(time || current_time_from_proper_timezone)
      end

引数のtimeがnilならcurrent_time_from_proper_timezoneを呼び出していますね。

timestamp_attributes_for_update_in_modelメソッドはどんな処理でしょうか。
このメソッドもコンソールから呼び出せるので、試してみましょう。

$ User.timestamp_attributes_for_update_in_model
=> ["updated_at"]

ふむふむ。後続の処理から察するに{ updated_at: Time.now } みたいな値が返りそうですが、、、

$ names = []
=> []

$ User.touch_attributes_with_time(*names, time: nil)
=> {"updated_at"=>2020-11-07 09:52:56 +0900}

当たってましたね。

他の呼び出し箇所も見てみましょう。

  private
    def _create_record
      if record_timestamps
        current_time = current_time_from_proper_timezone

        all_timestamp_attributes_in_model.each do |column|
          _write_attribute(column, current_time) unless _read_attribute(column)
        end
      end

      super
    end

ちょっと疲れてきたのでこれ以上追いませんが、おそらく実際にレコード作成時に呼び出されて
Time.nowの値が入るのがこの処理でしょう。

all_timestamp_attributes_in_modelメソッドの返り値は、

User.all_timestamp_attributes_in_model
=> ["created_at", "updated_at"]

となりますので、上記2つのカラムに対して、Time.nowの返り値が入ります。




Timeクラスの挙動については、以下の記事が詳しかったです。




勉強になりました。(コード追うの楽しい)

Exifとはなんぞや

こんにちは!kossyです!




さて、今回は恥ずかしながらExifという用語を初めて耳にしましたので、
備忘録としてブログに残してみたいと思います。




Exifってなんぞや

初耳でしたので調べてみました。

以下引用です。

Exifとは、デジタルカメラで撮影した画像データに、撮影条件に関する情報(メタデータ)を追加して保存できる、画像ファイル形式の規格のことである。
Exifでは、撮影した画像データと併せて、撮影した日時やデジタルカメラの機種、絞り値、画素数ISO感度、色空間、といった情報をまとめて記録することができる。

出典: https://www.weblio.jp/content/Exif

出典元には書いていませんが、位置情報なども格納されます。

よく、「撮影した画像をそのままネットにあげるのは危ない」と言われるのは、
Exifデータの存在があるから、というのが一つの理由です。(自宅を特定されるような方々はExifがなくても特定してしまいますが。)

現代のスマホにはほとんどの機種にGPS機能が搭載されていますが、
GPSをオンにした状態でカメラで写真を取ると、画像データの中に緯度と経度情報が格納されます。

Macで確認したい場合は、画像を選択し、「表示項目を増やす」をクリックすると、
確認ができます。
f:id:kossy-web-engineer:20201103180819p:plain

f:id:kossy-web-engineer:20201103180833p:plain

大手のSNSやブログはアップロード時にExifデータを削除してアップするようになっているようですが、
昔のサイトや自作のサイトなどではこの設定がなされていないものもあるようなので、
GPSをオンにした状態で撮影した写真をインターネットにアップロードするのは
避けた方が賢明でしょう。




勉強になりました。



参考にさせていただいた記事

この場を借りて御礼申し上げます。

Rubyで例外を起こすためのraiseとfailの違い

こんにちは!kossyです!




さて、今回はRubyで例外を起こすためのraiseとfailの違いについて、
ブログに残してみたいと思います。




環境
Ruby 2.6.3
MacOS catalina



両者に違いはない

調べてみましたが、raiseとfailには処理の違いはないようです。

Rubyスタイルガイドでは、raiseを使うことを推奨しているようです。



irbでも挙動を確かめてみました。

# StandardError例外をthrow

irb(main):001:0> fail StandardError
Traceback (most recent call last):

StandardError (StandardError)

irb(main):002:0> raise StandardError
Traceback (most recent call last):

StandardError (StandardError)

# 第二引数を渡しても挙動は一緒

fail StandardError, 'エラーです'
Traceback (most recent call last):

StandardError (エラーです)

irb(main):004:0> raise StandardError, 'エラーです'
Traceback (most recent call last):

StandardError (エラーです)

チームのコーディング規約に従うべきだとは思いますが、
Rubyコミュニティで一般的なのはraiseメソッドの方みたいなので、
特にこだわりがなければraiseメソッドの方を使えばいいかと思います。




勉強になりました。



参考にさせていただいた記事


この場を借りて御礼を申し上げます。

Railsのdependent: :restrict_with_error と :restrict_with_exception はなにが違うのか

こんにちは!kossyです!




さて、今回はRailsのdependentオプションに指定できる、
restrict_with_error と restrict_with_exception はなにが違うのかについて、
ブログに残してみたいと思います。




環境
Ruby 2.6.3
Rails 6.0.3
MacOS Catalina




そもそもdependentオプションとは?

まずはドキュメントを読んでみましょう。

Controls what happens to the associated objects when their owner is destroyed:

そのレコードが破棄されたときに、関連付けられたオブジェクトに何が起こるかを制御します。

出典: https://edgeguides.rubyonrails.org/association_basics.html#options-for-has-one-dependent

平たくいうと、レコード削除時に、関連づけられたオブジェクトをどうするかの制御を可能にするオプションですね。

いくつか割り当てられるオプションがありますが、今回はタイトルにもある通り、
restrict_with_errorとrestrict_with_exceptionに焦点を当ててみます。



restrict_with_errorとrestrict_with_exceptionの違い

ドキュメントを見てみます。

:restrict_with_exception causes an exception to be raised if there are any associated records.

関連するレコードがある場合、例外が発生します。

:restrict_with_error causes an error to be added to the owner if there are any associated objects.

関連するオブジェクトがある場合、所有者にエラーが追加されます。

出典: https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_many

restrict_with_exceptionの方は、レコードの削除メソッドが呼ばれたとき、
関連するオブジェクトがある場合は削除できずにActiveRecord::DeleteRestrictionError例外が発生します。

restrict_with_errorの方は、レコードの削除メソッドが呼ばれたとき、
関連するオブジェクトがある場合は削除できずにレコードのerrorsにエラーメッセージ が追加されます。


検証コード

Rspecで検証コードを書いてみました。

前提として、Rspecとfactory_botを用いていて、

user has_many :daily_reports, dependent: :restrict_with_exception
user has_many :schedules, dependent: :restrict_with_error

という関連が組まれているとします。

以下、コード全晒しです。

  describe 'association' do
    describe 'daily_reports' do
      context 'dependent: :restrict_with_exception' do
        it 'expect raise ActiveRecord::DeleteRestrictionError' do
          user = create :user
          create :daily_report, user: user

          expect { user.destroy! }.to raise_error(ActiveRecord::DeleteRestrictionError)
          expect(user.errors.full_messages).to eq []
        end
      end
    end

    describe 'schedules' do
      context 'dependent: :restrict_with_error' do
        it 'expect raise ActiveRecord::RecordNotDestroyed' do
          user = create :user
          create :schedule, user: user

          expect { user.destroy! }.to raise_error(ActiveRecord::RecordNotDestroyed)
          expect(user.errors.full_messages.any?).to eq true
        end
      end
    end
  end

restrict_with_exceptionの場合は、ActiveRecord::DeleteRestrictionError例外が発生し、
user.errors.full_messagesの返り値は空になっていることが確認できました。

restrict_with_errorの場合は、ActiveRecord::RecordNotDestroyed例外が発生し、
user.errors.full_messagesにはエラーメッセージが格納されたことが確認できました。



どう使い分けるか

エラーメッセージ をユーザーに見せたい場合は、restrict_with_errorを使って、
そうでない場合はrestrict_with_exceptionを使えばいいように思います。



大いに参考にさせていただいた記事


この場を借りて御礼申し上げます。