ruby-on-railsdevisewarden

Rails saving belongs_to association only if valid


We've got a Devise User model that we need to create and associate with an organization upon sign up.

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :confirmable

  acts_as_tenant(:tenant)
  acts_as_paranoid
  belongs_to :organization, :autosave => true

  before_validation :create_organization

  def create_organization
      org = Organization.new(name: self.email)
      org.uuid = UUIDTools::UUID.random_create.to_s
      if org.save
        self.organization_id = org.uuid
      end
  end
end

The problem we're having is that if something fails on the User form (mismatched passwords, etc) we are left with an orphaned Organization. On the flip side, if we wrap the create organization code in a

Warden::Manager.after_authentication do |user,auth,opts|
   make org
end

Then the Organization must exist error gets placed in the errors array and printed to the screen, which we also don't want because Users don't really need to know about the Organization behind the scenes.

How do you set the Organization association so that the User becomes valid but not save the Users Organization association until you know everything else is valid as well?


Solution

  • Replace create with build, and don't persist the record until the parent record is valid. A has_one :foo association defines a build_foo method for you, which is what you should use to build your organization. It will automatically place the organization in user.organization, you shouldn't be juggling foreign keys manually when working with ActiveRecord objects:

      before_validation :build_default_organization
    
      def build_default_organization
        build_organization(name: email, uuid: UUIDTools::UUID.random_create.to_s)
      end
    

    Rails will automatically save this record for you, inside a transaction that includes the saving of the parent User record, and it will automatically set all the foreign keys correctly.

    Another option is to use accepts_nested_attributes and pass through organization_attributes when you're creating your user.

    class User < ApplicationRecord
      belongs_to :organization, :autosave => true
    
      accepts_nested_attributes_for :organization
    end
    
    User.create!(
      email: 'test_user@test.com',
      organization_attributes: {
        name: 'test_user@test.com',
        uuid: UUIDTools::UUID.random_create.to_s
      )
    )