0

Is it possible using accepts_nested_attributes_for in a has_many through: association to create children only if not duplicates. Otherwise create only the relationship in the join table?

I have a List which can have many Email. The inverse is possible too. See the models below.

# models/list.rb
class List < ActiveRecord::Base
  has_many :emails_lists
  has_many :emails, through: :emails_lists

  accepts_nested_attributes_for :emails, allow_destroy: true
end

# models/email.rb
class Email < ActiveRecord::Base
  has_many :emails_lists
  has_many :lists, through: :emails_lists

  validates :name, uniqueness: true

  accepts_nested_attributes_for :lists, allow_destroy: true
end

# models/emails_list.rb
class EmailsList < ActiveRecord::Base
  belongs_to :email
  belongs_to :list

  validates :email_id, uniqueness: {scope: :list_id}
end

Here is the List view _form.

# views/lists/_form.html.haml
= nested_form_for @list do |f|
  - if @list.errors.any?
    #error_explanation
      %h2= "#{pluralize(@list.errors.count, "error")} prohibited this list from being saved:"
      %ul
        - @list.errors.full_messages.each do |msg|
          %li= msg

  .field
    = f.label :name
    = f.text_field :name
  .field
    = f.label :description
    = f.text_area :description
  %fieldset
    %legend Email
    = f.fields_for :emails do |email|
      .field.email
        = email.label :name
        %p
          = email.text_field :name
          = email.link_to_remove "Remove this email"
    %p
      %i.fa.fa-plus
        = f.link_to_add "Add an email", :emails
  .actions
    = f.submit 'Save'

Here is the action concerned 'update' and the list_params method.

# PATCH/PUT /lists/1
# PATCH/PUT /lists/1.json
def update
  # binding.pry
  respond_to do |format|
    if @list.update(list_params)
      format.html { redirect_to @list, notice: 'List was successfully updated.' }
      format.json { render :show, status: :ok, location: @list }
    else
      format.html { render :edit }
      format.json { render json: @list.errors, status: :unprocessable_entity }
    end
  end
end

# Never trust parameters from the scary internet, only allow the white list through.
def list_params
  params.require(:list).permit(:name, :description, emails_attributes: [:id, :name, :_destroy])
end

In the view form I can add new emails to a list. The problem is when I try to add a duplicate email. The validation and save is rejected.

Here I insert a duplicate via the Rails console.

2.1.6 :001 > l = List.create
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "lists" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", "2015-05-20 05:52:38.567388"], ["updated_at", "2015-05-20 05:52:38.567388"]]
   (1.1ms)  commit transaction
 => #<List id: 7, name: nil, description: nil, created_at: "2015-05-20 05:52:38", updated_at: "2015-05-20 05:52:38"> 
2.1.6 :002 > l.emails.build({name: 'foo@test.com'})
 => #<Email id: nil, name: "foo@test.com", company_id: nil, created_at: nil, updated_at: nil> 
2.1.6 :003 > l.save
   (0.1ms)  begin transaction
  Email Exists (0.1ms)  SELECT  1 AS one FROM "emails" WHERE "emails"."name" = 'foo@test.com' LIMIT 1
  SQL (0.3ms)  INSERT INTO "emails" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "foo@test.com"], ["created_at", "2015-05-20 05:54:59.228313"], ["updated_at", "2015-05-20 05:54:59.228313"]]
  EmailsList Exists (0.1ms)  SELECT  1 AS one FROM "emails_lists" WHERE ("emails_lists"."email_id" = 18 AND "emails_lists"."list_id" = 7) LIMIT 1
  SQL (0.1ms)  INSERT INTO "emails_lists" ("list_id", "email_id") VALUES (?, ?)  [["list_id", 7], ["email_id", 18]]
   (1.3ms)  commit transaction
 => true 
2.1.6 :004 > l.emails.build({name: 'foo@test.com'})
 => #<Email id: nil, name: "foo@test.com", company_id: nil, created_at: nil, updated_at: nil> 
2.1.6 :005 > l.save
   (0.1ms)  begin transaction
  Email Exists (0.2ms)  SELECT  1 AS one FROM "emails" WHERE "emails"."name" = 'foo@test.com' LIMIT 1
   (0.1ms)  rollback transaction
 => false 

The error:

Emails name has already been taken

My goal is:

  • If Email name is not already present in 'email' create an Email in 'email' table. Then create the relationship between Email and List in 'emails_lists'.
  • If Email name is a duplicate find the duplicate and create only the relationship between Email and List in 'emails_lists'.

Edit

I find this post which describe a similar problem: rails validate nested attributes

Hack

I find a custom hack to solve my problem. I think the solution is portable for other cases. I don't think it is the best practice but it seems to work in add/delete cases.

def emails_attributes=(hash)
  hash.each do |sequence,email_values|
    if !email_values.include?(:id) && email = Email.find_by_name(email_values[:name])
      EmailsList.create(email_id: email.id, list_id: self.id)
      email_values['id'] = email.id
    end
  end
  super
end
Community
  • 1
  • 1
gsorbier
  • 1
  • 1
  • Post your action which is dealing with the case – RAJ May 20 '15 at 06:05
  • it sounds like you want to use find_or_create_by with accepts_nested_attributes_for right? http://stackoverflow.com/questions/3579924/accepts-nested-attributes-for-with-find-or-create – PhilVarg May 22 '15 at 16:32
  • The link you gave me shows several hacks to try to achieve the problem. Some doesn't work in all situation (add/update/delete). The best explained solution is the last from @Dustin M. But it does a lot of code to achieve a simple goal. None of the solutions fit to me, I – gsorbier May 28 '15 at 13:54

0 Answers0