Authlogic使ってるモデルで認証に関係ないnullableなemailカラム追加したらvalidationエラーが出る件

More than 3 years have passed since last update.


状況説明

ログインは Facebook アカウントの認証のみのつもり。

なので User モデルは実体がなく、ほかのモデルとのリレーションと、後は UserSession の管理だけの空っぽのモデル。


db/schema.rb

  create_table "users", force: true do |t|

t.string "persistence_token", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

関連モデルもまだ作っていないので、本当にこれだけ。


app/models/user.rb

class User < ActiveRecord::Base

acts_as_authentic
end

で、 "任意で構わないけど email を保存したい" という話になったので、 nullable な email カラムを追加。


db/schema.rb

  create_table "users", force: true do |t|

t.string "persistence_token", null: false
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

そうしたら email が空だとバリデーションエラーになって、保存できなくなった。

> User.create!

(0.2ms) BEGIN
User Exists (0.3ms) SELECT 1 AS one FROM "users" WHERE "users"."persistence_token" = '689...' LIMIT 1
(0.2ms) ROLLBACK
ActiveRecord::RecordInvalid: Validation failed: Email should look like an email address.

え、いやいやいやいや。

そんな validation 作ってないし!

nullable だし!

User モデルだって空っぽだし!

って混乱したんだけれども。

どうも Authlogic を使うための acts_as_authentic が怪しい。というか User モデルにはそれしかないし。

という調査内容と、回避方法についての話。

前置き完了。

これの調査した時は、 authlogic 3.4.2 でした。

Rails は 4.2.0 系になってたかな……(うろ覚え)。


原因

authlogic/acts_as_authentic/base.rb を見ると


authlogic/acts_as_authentic/base.rb

::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Base

::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Email
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::LoggedInStatus
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Login
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::MagicColumns
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Password
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PerishableToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PersistenceToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::RestfulAuthentication
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SessionMaintenance
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SingleAccessToken
::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::ValidationsScope

なんかすごい勢いで include されてました。

Authlogic::ActsAsAuthentic::Email とかあるので、それを見てみる。


authlogic/acts_as_authentic/email.rb

      module Methods

def self.included(klass)
klass.class_eval do
if validate_email_field && email_field
validates_length_of email_field, validates_length_of_email_field_options
validates_format_of email_field, validates_format_of_email_field_options
validates_uniqueness_of email_field, validates_uniqueness_of_email_field_options
end
end
end
end

おまえたちか!


回避方法を探る

authlogic/acts_as_authentic/base.rbacts_as_authentic メソッドがいる


authlogic/acts_as_authentic/base.rb

      module Config

# This includes a lot of helpful methods for authenticating records which The Authlogic::Session module relies on.
# To use it just do:
#
# class User < ActiveRecord::Base
# acts_as_authentic
# end
#
# Configuration is easy:
#
# acts_as_authentic do |c|
# c.my_configuration_option = my_value
# end
#
# See the various sub modules for the configuration they provide.
def acts_as_authentic(unsupported_options = nil, &block)
# Stop all configuration if the DB is not set up
return if !db_setup?

if !unsupported_options.nil?
raise ArgumentError.new(
"You are using the old v1.X.X configuration method for Authlogic. Instead of passing a hash of " +
"configuration options to acts_as_authentic, pass a block: acts_as_authentic { |c| c.my_option = my_value }"
)
end

yield self if block_given?
acts_as_authentic_modules.each { |mod| include mod }
end

# (略)


unsupported_options ってあるから一瞬喜んだんだけど、これは古いバージョンの書き方で、今はブロックを使えよってことらしい。

authlogic/acts_as_authentic/email.rb を見てみると、こっちも Config を持っていて、個別に設定できる項目があるらしい。


authlogic/acts_as_authentic/email.rb

      # Configuration to modify how Authlogic handles the email field.

module Config
# The name of the field that stores email addresses.
#
# * <tt>Default:</tt> :email, if it exists
# * <tt>Accepts:</tt> Symbol
def email_field(value = nil)
rw_config(:email_field, value, first_column_to_exist(nil, :email, :email_address))
end
alias_method :email_field=, :email_field

# Toggles validating the email field or not.
#
# * <tt>Default:</tt> true
# * <tt>Accepts:</tt> Boolean
def validate_email_field(value = nil)
rw_config(:validate_email_field, value, true)
end
alias_method :validate_email_field=, :validate_email_field

# (略)


なるほど。

email とか email_address というカラムがあると、勝手に email 用のバリデーションしてくれると。

email_field に nil を指定しても、デフォルトのカラム名が使われてしまうので、今回のケースにそちらは仕えない。

(例えば main_address みたいなカラムに email を保存したいという時に、こちらの設定が使える)

validate_email_field はデフォルトが true になってるけれども、これが false になっていれば、今は必要ないバリデーションを動作させずに済みそう。


回避!

acts_as_authentic にブロックを渡して validate_email_fieldfalse に指定する。


app/models/user.rb

 class User < ActiveRecord::Base

- acts_as_authentic
+ acts_as_authentic do |config|
+ config.validate_email_field = false
+ end

これで。

pry> User.create!

(0.1ms) BEGIN
User Exists (0.6ms) SELECT 1 AS one FROM "users" WHERE "users"."persistence_token" = '301...' LIMIT 1
SQL (0.5ms) INSERT INTO "users" ("created_at", "persistence_token", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2014-10-03 03:52:22.984187"], ["persistence_token", "301..."], ["updated_at", "2014-10-03 03:52:22.984187"]]
(0.3ms) COMMIT
=> #<User:0x007ffbdfd1bb30
id: 1,
created_at: Fri, 03 Oct 2014 03:52:22 UTC +00:00,
updated_at: Fri, 03 Oct 2014 03:52:22 UTC +00:00,
persistence_token: "3011...",
email: nil>

できた!