Rails has_many :through association: save instance

2019-07-14 04:14发布

问题:

In our Rails app, there are 3 models:

class User < ActiveRecord::Base
  has_many :administrations, dependent: :destroy
  has_many :calendars, through: :administrations
end

class Administration < ActiveRecord::Base
  belongs_to :user
  belongs_to :calendar
end

class Calendar < ActiveRecord::Base
  has_many :administrations, dependent: :destroy
  has_many :users, through: :administrations
end

And here are the corresponding migrations:

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :first_name
      t.string :last_name
      t.string :email

      t.timestamps null: false
    end
  end
end

class CreateAdministrations < ActiveRecord::Migration
  def change
    create_table :administrations do |t|
      t.references :user, index: true, foreign_key: true
      t.references :calendar, index: true, foreign_key: true
      t.string :role

      t.timestamps null: false
    end
  end
end

class CreateCalendars < ActiveRecord::Migration
  def change
    create_table :calendars do |t|
      t.string :name

      t.timestamps null: false
    end
  end
end

We have created the calendar#create action as follows:

def create
    @calendar = current_user.calendars.build(calendar_params)
    if @calendar.save
      flash[:success] = "Calendar created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

And the corresponding _calendar_form.html.erb partial:

<%= form_for(@calendar) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_field :name, placeholder: "Your new calendar name" %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

This "works" since, when we create a new calendar through the form, it does show up in Rails console, when we type Calendar.all.

However, it seems like no new @administration is being created and the Administration table is not updated, as nothing returns when we type Administration.all in the console.

We thought the Administration table, which is the join table between the User table and the Calendar table, and which respectively contains user_id and calendar_id columns, would be updated automatically when creating a new calendar.

How can we achieve this? Do we need to create a specific administration#create action?

UPDATE: based on the comments and the answers, we implemented the following CalendarsController:

class CalendarsController < ApplicationController

  def create
    @calendar = current_user.calendars.build(calendar_params)
    if @calendar.save
      current_user.administrations << @calendar
      @calendar.administration.role = 'creator'
      flash[:success] = "Calendar created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

...

However, this returns the following error:

ActiveRecord::AssociationTypeMismatch in CalendarsController#create

unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
            message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
            raise ActiveRecord::AssociationTypeMismatch, message
          end
        end

app/controllers/calendars_controller.rb:6:in `create'

Are we missing something?

回答1:

If you use current_user.calendars.build(calendar_params), you will only get new calendar, no administration.

If you use current_user.calendars.create(calendar_params), you will get both administration and calendar saved into database.

If you want to check whether calendar is successfully saved first, I use this approach:

def create
  @calendar = Calendar.build(calendar_params)
  if @calendar.save
    current_user.administrations << @calendar
    flash[:success] = "Calendar created!"
    redirect_to root_url
  else
    render 'static_pages/home'
  end
end

UPDATED:

There is an error to associate calendar to user. This should be the correct one:

def create
  @calendar = current_user.calendars.build(calendar_params)
  if @calendar.save
    current_user.calendars << @calendar
    flash[:success] = "Calendar created!"
    redirect_to root_url
  else
    render 'static_pages/home'
  end
end


回答2:

Yes, you absolutely do! You're not getting a new Administration object because you're not asking the system for one.

If you really, truly needed to create a new Administration object every time a Calendar was created, you could do something like this:

class Calendar < ActiveRecord::Base
  attr_reader :administration

  def initialize(params)
    @administration = Administration.new(params[:id])
  end
end

class Administration < ActiveRecord::Base
  def initialize(id_param_from_calendar)
    calendar_id = id_param_from_calendar
  end
end

Obviously this isn't a best practice, as it injects dependencies in your code. Now, no matter what, your Calendar class is aware that an Administration class not only exists, but uses its id column to initialize. This was just an example to answer your question.

Another example would be to include an after_initialize hook in your Calendar class like so (please note the autosave: true which is useful for ensuring the automatic saving of child objects):

class Calendar < ActiveRecord::Base
  has_many :administrations, autosave: true, dependent: :destroy

  after_initialize :init_admins

  def init_admins
    # you only wish to add admins on the newly instantiated Calendar instances
    if new_record?
      3.times { Administration.new self }
    end
  end

end

What's more, you could also include the above logic in an after_create hook. The only difference is that after_create occurs after an object's creation and after_initialize occurs after an object is either instantiated or loaded from the database.