Andrew Andrew - 3 months ago 8
SQL Question

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"=>"_______________________@gmail.com", "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('_______________________@gmail.com')) 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 ('_______________________@gmail.com', '$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 _________@gmail.com (1966ms)
Date: Mon, 18 Apr 2011 21:01:55 -0500
From: __________
Reply-To: ___________
To: _____________
Message-ID: <4daced1352c84_1ff5817d6b04978cd@Titan.local.mail>
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: _________@gmail.com. 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('_______________________@gmail.com')) 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('_______________________@gmail.com')) 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('_______________________@gmail.com')) 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('_______________________@gmail.com')) 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('_______________________@gmail.com')) 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

Answer

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.