Reputation: 283
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
Reputation: 8630
The "Users organization must exist" error should not occur. ActiveRecord is "smart," in that it should execute two INSERT
s. 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
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
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