Andrew
Andrew

Reputation: 43123

Rails 3: Infinite SQL query?

I changed my User model to accept_nested_attributes_for Profile, and I'm trying to create the User and Profile at the same time. I'm using Devise for authentication.

This seems to be working -- except for one giant gotcha...

Every time I create a new user it crashes the app with "Illegal Instruction", and when I check the log it looks like this...

Started POST "/users" for 127.0.0.1 at 2011-04-18 21:01:54 -0500
  Processing by UsersController#create as HTML
  Parameters: {"utf8"=>"‚úì", "authenticity_token"=>"Rua6PUxnE4a4TvaFcVMfmycw8Y9AFRjEsXVrqwWC2EM=", "user"=>{"email"=>"[email protected]", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "profile_attributes"=>{"first_name"=>"Name", "last_name"=>"Tester"}, "student_claimed"=>"false", "school"=>"", "invite_code"=>"Texas!", "terms_of_service"=>"1"}, "commit"=>"Create Account!"}
  [1m[35mSQL (0.3ms)[0m   SELECT name
 FROM sqlite_master
 WHERE type = 'table' AND NOT name = 'sqlite_sequence'
  [1m[36mSQL (0.3ms)[0m  [1m SELECT name
 FROM sqlite_master
 WHERE type = 'table' AND NOT name = 'sqlite_sequence'
[0m
  [1m[35mUser Load (0.2ms)[0m  SELECT "users"."id" FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]')) LIMIT 1
  [1m[36mInvitation Load (0.1ms)[0m  [1mSELECT "invitations".* FROM "invitations" WHERE "invitations"."code" = 'Texas!' LIMIT 1[0m
  [1m[35mUser Load (0.1ms)[0m  SELECT "users".* FROM "users" WHERE "users"."confirmation_token" = 'duALIT6yCL5ShpMvbw79' LIMIT 1
  [1m[36mRole Load (0.3ms)[0m  [1mSELECT "roles".* FROM "roles" WHERE "roles"."name" = 'member' LIMIT 1[0m
  [1m[35mAREL (0.3ms)[0m  UPDATE "invitations" SET "remaining_uses" = 9993, "updated_at" = '2011-04-19 02:01:54.506243' WHERE "invitations"."id" = 1
  [1m[36mAREL (0.2ms)[0m  [1mINSERT INTO "users" ("email", "encrypted_password", "reset_password_token", "remember_token", "remember_created_at", "sign_in_count", "current_sign_in_at", "last_sign_in_at", "current_sign_in_ip", "last_sign_in_ip", "created_at", "updated_at", "plan_code", "confirmation_token", "confirmed_at", "confirmation_sent_at", "student_claimed", "student_confirmed", "school", "invitation_id") VALUES ('[email protected]', '$2a$10$7qzC7T6b1kLiXvPSkMRkduCFClBznDWnnOu7I1ssU8blB9NMJznn2', NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL, '2011-04-19 02:01:54.509656', '2011-04-19 02:01:54.509656', NULL, 'duALIT6yCL5ShpMvbw79', NULL, '2011-04-19 02:01:54.437796', 'f', 'f', '', 1)[0m
  [1m[35mSQL (0.1ms)[0m  INSERT INTO "roles_users" ("role_id", "user_id") VALUES (3, 6)
Rendered devise/mailer/confirmation_instructions.html.erb (0.9ms)

Sent mail to [email protected] (1966ms)
Date: Mon, 18 Apr 2011 21:01:55 -0500
From: __________
Reply-To: ___________
To: _____________
Message-ID: <[email protected]>
Subject: Please confirm your email address
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<p>Name,</p>

<p>You registered with the email address: [email protected]. You can confirm your account through the link below:</p>

<p><a href="http://localhost:3000/users/confirmation?confirmation_token=duALIT6yCL5ShpMvbw79">Confirm my account</a></p>

<p>Thanks for signing up!</p>

  [1m[36mAREL (0.2ms)[0m  [1mINSERT INTO "profiles" ("first_name", "last_name", "created_at", "updated_at", "user_id", "avatar_file_name", "avatar_content_type", "avatar_file_size", "avatar_updated_at", "address1", "city", "state", "country", "zip") VALUES ('Name', 'Tester', '2011-04-19 02:01:57.266502', '2011-04-19 02:01:57.266502', 6, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)[0m
[paperclip] Saving attachments.
  [1m[35mUser Load (0.1ms)[0m  SELECT "users"."id" FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]')) AND ("users".id <> 6) LIMIT 1
  [1m[36mUser Load (1.6ms)[0m  [1mSELECT "users".* FROM "users" WHERE "users"."id" = 6 LIMIT 1[0m
  [1m[35mProfile Load (1.6ms)[0m  SELECT "profiles".* FROM "profiles" WHERE ("profiles".user_id = 6)
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "users"."id" FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]')) AND ("users".id <> 6) LIMIT 1[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "users".* FROM "users" WHERE "users"."id" = 6 LIMIT 1
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "profiles".* FROM "profiles" WHERE ("profiles".user_id = 6)[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "users"."id" FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]')) AND ("users".id <> 6) LIMIT 1
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "users".* FROM "users" WHERE "users"."id" = 6 LIMIT 1[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "profiles".* FROM "profiles" WHERE ("profiles".user_id = 6)
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "users"."id" FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]')) AND ("users".id <> 6) LIMIT 1[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "users".* FROM "users" WHERE "users"."id" = 6 LIMIT 1
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "profiles".* FROM "profiles" WHERE ("profiles".user_id = 6)[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "users"."id" FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]')) AND ("users".id <> 6) LIMIT 1
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "users".* FROM "users" WHERE "users"."id" = 6 LIMIT 1[0m
  [1m[35mCACHE (0.0ms)[0m  SELECT "profiles".* FROM "profiles" WHERE ("profiles".user_id = 6)

  ... and so on for about 100 more lines ...

  [1m[35mCACHE (0.0ms)[0m  SELECT "users".* FROM "users" WHERE "users"."id" = 6 LIMIT 1
  [1m[36mCACHE (0.0ms)[0m  [1mSELECT "profiles".* FROM "profiles" WHERE ("profiles".user_id = 6)[0m
  [1m[36mSQL (0.3ms)[0m  [1m SELECT name

So, this wasn't happening before I started accepting nested attributes... and I'm pretty confused as to why it's happening now. Does anyone have any insight into how to debug this and fix the problem?

Thanks!

--EDIT--

User Model:

class User < ActiveRecord::Base
  # RELATIONSHIPS
  has_one :profile, :dependent => :destroy
  has_many :photos
  has_many :votes
  has_many :voted_photos, :through => :votes, :source => :photo
  has_many :ratings
  has_many :rated_photos, :through => :ratings, :source => :photo
  has_many :comments
  has_and_belongs_to_many :roles
  has_many :assignments
  has_many :collections, :through => :assignments
  belongs_to :invitation

  accepts_nested_attributes_for :profile

  # VIRTUAL ATTRIBUTES
  attr_accessor :invite_code

  # AUTHENTICATION
  devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable, :confirmable

  # SECURITY
  attr_accessible :email, :password, :password_confirmation, :remember_me, :confirmed_at, :invite_code, :student_claimed, :school, :terms_of_service, :profile_attributes

  # FILTERS
  before_create :set_role_to_member, :set_invitation
  after_save :update_recurly_account, :unless => Proc.new { Rails.env.test? }

  # VALIDATIONS
  validates_acceptance_of :terms_of_service, :message => "You must agree to the terms of service in order to create an account."
  validate :invitation_status, :on => :create
  validates_presence_of :profile
  validates_associated :profile

  # DELEGATES
  delegate  :first_name, :last_name, :full_name,
            :to => :profile,
            :allow_nil => true

  # ROLES
  def set_role_to_member
    self.roles << Role.find_by_name('member')
  end

  def has_role?( r )
    !roles.find_by_name( r ).nil?
  end

  def list_roles
    list = []
    roles.all.each do |r|
      list << r.name
    end
    list.join(', ')
  end

  # DEVISE RELATED
  # Hook up recurly account after confirmation
  def confirm!
    self.setup_recurly_account unless Rails.env.test?

    if student_claimed && validate_student_email
      self.student_confirmed = true
      self.save
    end

    super
  end

  protected
  # Don't require password on update
  def password_required?
    !persisted? || password.present? || password_confirmation.present?
  end

  public

  # RECURLY RELATED

  def setup_recurly_account
    ...
  end

  private

    def update_recurly_account
      ...
    end

    def validate_student_email
      self.email =~ /\.edu$/ ? true : false
    end

    def invitation_status
      ...
    end

    def set_invitation
      ...
    end

end

Profile Model

class Profile < ActiveRecord::Base
  include Helpers::AssetStorage

  # RELATIONSHIPS
  belongs_to :user

  stores_file_as :avatar,
                    :styles =>  { :tenth => "87x87#", :eighth => "106x106#" },
                    :filename_interpolation => "avatars/:user_id/:id_:style.:extension",
                    :default_url => '/images/no_avatar_:style.png'

  # VALIDATIONS
  validates_presence_of :first_name, :last_name

  # CALLBACKS
  after_update :save_user

  def full_name
    [first_name,last_name].join(" ")
  end

  private

    def save_user
      self.user.save!
    end

end

Upvotes: 1

Views: 507

Answers (1)

PeterWong
PeterWong

Reputation: 16011

You don't need the save_user callback for Profile model.

When doing user.save, it automatically save user.profile. Due to the callback, the user.profile saved, and it calls it's user to save again. And the user save, it also save his profile......

That's the loop.

So the simplest modification would be remove the after_update callback in Profile model.

If you want to save the profile only, use profile.save. If the user object has updates too, use user.save or profile.user.save.

Upvotes: 1

Related Questions