nsommer
nsommer

Reputation: 283

Creating model and nested model (1:n) at once with ActiveRecord

My Rails5 application has an organization model and a user model (1:n relationship). The workflow of creating an organization should include the creation of the organization's first user as well. I thought this would be able with ActiveRecord through nested models, however the create action fails with the error message "Users organization must exist".

class Organization < ApplicationRecord
    has_many :users, dependent: :destroy
    accepts_nested_attributes_for :users
end

class User < ApplicationRecord
    belongs_to :organization
end

class OrganizationsController < ApplicationController
    def new
        @organization = Organization.new
        @organization.users.build
    end

    def create
        @organization = Organization.new(organization_params)
        if @organization.save
            redirect_to @organization
        else
          render 'new'
        end
    end

    def organization_params
        params.require(:organization).permit(:name, users_attributes: [:name, :email, :password, :password_confirmation])
    end
end

In the view I use the <%= f.fields_for :users do |user_form| %> helper.

Is this a bug on my side, or isn't this supported by ActiveRecord at all? Couldn't find anything about it in the rails guides. After all, this should be (theoretically) possible: First do the INSERT for the organization, then the INSERT of the user (the order matters, to know the id of the organization for the foreign key of the user).

Upvotes: 1

Views: 4359

Answers (3)

ybakos
ybakos

Reputation: 8630

The "Users organization must exist" error should not occur. ActiveRecord is "smart," in that it should execute two INSERTs. First, it will save the model on the has_many side, so that it has an id, and then it will save the model on the belongs_to side, populating the foreign key value. The problem is actually caused by a bug in accepts_nested_attributes_for in Rails 5 versions prior to 5.1.1. See https://github.com/rails/rails/issues/25198 and Trouble with accepts_nested_attributes_for in Rails 5.0.0.beta3, -api option.

The solution is to use the inverse_of: option or, better yet, upgrade to Rails 5.1.1.

You can prove that this is true by removing the accepts_nested_attributes_for in your Organization model and, in the Rails console, creating a new Organization model and a new User model, associating them (eg myorg.users << myuser) and trying a save (eg myorg.save). You'll find that it will work as expected.

Upvotes: 0

nsommer
nsommer

Reputation: 283

As described in https://github.com/rails/rails/issues/18233, Rails5 requires integrity checks. Because I didn't like a wishy-washy solution like disabling the integrity checks, I followed DHH's advice from the issue linked above:

I like aggregation through regular Ruby objects. For example, we have a Signup model that's just a Ruby object orchestrating the build process. So I'd give that a go!

I wrote a ruby class called Signup which encapsulates the organization and user model and offers a save/create interface like an ActiveRecord model would. Furthermore, by including ActiveModel::Model, useful stuff comes in to the class for free (attribute hash constructor etc., see http://guides.rubyonrails.org/active_model_basics.html#model).

# The Signup model encapsulates an organization and a user model.
# It's used in the signup process and helps persisting a new organization
# and a referenced user (the owner of the organization).
class Signup
  include ActiveModel::Model

  attr_accessor :organization_name, :user_name, :user_email, :user_password, :user_password_confirmation

  # A save method that acts like ActiveRecord's save method.
  def save
    @organization = build_organization

    return false unless @organization.save

    @user = build_user

    @user.save
  end

  # Checks validity of the model.
  def valid?
    @organization = build_organization
    @user = build_user

    @organization.valid? and @user.valid?
  end

  # A create method that acts like ActiveRecord's create method.
  # This builds the object from an attributes hash and saves it.
  def self.create(attributes = {})
    signup = Signup.new(attributes)
    signup.save
  end

  private

  # Build an organization object from the attributes.
  def build_organization
    @organization = Organization.new(name: @organization_name)
  end

  # Build a user object from the attributes. For integritiy reasons,
  # a organization object must already exist.
  def build_user
    @user = User.new(name: @user_name, email: @user_email, password: @user_password, password_confirmation: @user_password_confirmation, organization: @organization)
  end
end

Special thanks to @engineersmnky for pointing me to the corresponding github issue.

Upvotes: 1

bkunzi01
bkunzi01

Reputation: 4561

You're looking for "Association Callbacks". Once you send those params to your organization model you have access to them inside that model. If everytime an organization is created there will be a new user assigned to it you can just do the following in your Organization Model:

 has_many :users, dependent: :destroy, after_add: :create_orgs_first_user

 attr_accessor: :username #create virtual atts for all the user params and then assign them as if they were organizational attributes in the controller.  This means changing your `organization_params` method to not nest user attributes inside the array `users_attributes`


  def create_orgs_first_user
    User.create(name: self.username, organization_id: self.id,  etc.) #  You can probably do self.users.create(params here) but I didn't try it that way. 
  end

Upvotes: 0

Related Questions