nsommer nsommer - 2 months ago 12
Ruby Question

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).

Answer

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.