[RUBY] Manipulate multiple models on form objects [De-accepts_nested_attributes_for]

I wrote it very carefully, so it has a lot of volume. Skip what you know: thumbs up:

Thing you want to do

-I want to operate multiple models without using accepts_nested_attributes_for. ・ I want to update not only new data.

Advance preparation

What is accepts_nested_attributes_for?

A method that allows you to update the records of the associated model at once.

Example) When sending a message with multiple images attached er_for_message.png

In the above case, you can save the message and the image associated with it. It's convenient. However, ** Actually, this method is not very popular **: joy: (Click here for details)

Therefore, I will introduce alternative form objects.

What is a form object?

A separate class for processing forms, separated from the model.

It allows you to update multiple records without ** accepts_nested_attributes_for. ** ** If you want to know more about other merits and principles, please see this article. The explanation is insanely easy to understand: pray:

Goal of this article

Implement a function that allows the user (parent) to associate a message (child) with an image (grandchild) and send it.

Change the parent, child, and grandchild models to suit your situation.

Omitted part

-Routing settings ・ Carrier wave settings required for image posting ・ Creating a table for each model

Implementation example

Let's take a look!

model

user.rb


class User < ApplicationRecord
  has_many :messages
end

message.rb


class Message < ApplicationRecord
  has_many :pictures
  belongs_to :user
  #The validation of the form written here moves to the form object.
end

picture.rb


class Picture < ApplicationRecord
  belongs_to :message
  #Those who do not post images do not need the following description.
  mount_uploader :picture, PictureUploader
end

There is no description about validation. It's refreshing and easy to read: relaxed:

controller

I would like you to pay attention to only the following two points.

-Update the information of @ message based on the value passed from the parameter in assigns_attributes. -MessageForm is a class for form objects, which will be explained later.

messages_controller.rb


  #New creation screen
  def new
    @message = MessageForm.new
  end

  #Create New
  def create
    @message = MessageForm.new
    @message.assign_attributes(message_form_params)
    if @message.save
      #Success / failure processing
    end
  end

  #Editing screen
  def edit
    # params[:id]For the part, enter a value that can identify the object to be edited.
    @message = MessageForm.new(message = Message.find(params[:id]))
  end

  #Edit
  def update
    @message = MessageForm.new(message = Message.find(params[:id]))
    @message.assign_attributes(message_form_params)
    if @message.save
      #Success / failure processing
    end
  end

  private
    #Strong parameters
    def message_form_params
      params.require(:message_form).permit(:body, pictures_attributes: [:picture]).merge(user_id: current_user)
    end

The strong parameter pictures_attributes will be explained next.

View

This is the new message creation screen.

ruby:new.html.erb


<%= form_with model: @message, url:Path for message composition, local: true do |f| %>

  <%= f.text_area :body %>

  <%= f.fields_for :pictures do |picture| %>
    <%= picture.file_field :picture, multiple: "multiple", name: "message_form[pictures_attributes][][picture]" %>
  <% end %>

  <%= f.submit "Send" %>

<% end %>

The explanation will focus on the parameters.

First of all, suppose you typed "OK" in the text area and sent it. The parameters at this time are as follows. (Please check at the terminal.)

Parameters: {"authenticity_token"=>"Abbreviation", "message_form"=>{"body"=>"OK"}, "button"=>""}

It contains the value you entered in the body column.

Here, it is "message_form " because @ message is generated from the MessageForm class. (This will happen automatically.)

Next, if you want to add image information to this, you want to make it look like the following.

Parameters: {"authenticity_token"=>"Abbreviation", "message_form"=>{Image information, "body"=>"OK"}, "button"=>""}

So, let's use the name attribute of file_field.

name: "message_form[pictures_attributes][][picture]"

If you write like this, the parameters when posting two images are as follows. (The image information is long, so it is omitted.)

 Parameters: {"authenticity_token"=>"Abbreviation", "message_form"=>{"pictures_attributes"=>[{"picture"=>1st image information}, {"picture"=>2nd image information}], "body"=>"OK"}, "button"=>""}

You can see that the image information is included under " message_form ".

Also, " pictures_attributes "=> [{"picture "=> corresponds to the controller's strong parameters.

For now, it looks like you can pass the value to the controller: relaxed:

** By the way, you need to be careful in the case of the edit screen. ** ** The reason is written in this article, so please refer to it if you have time.

Class for form object

This is the most complicated part. I will explain it in four parts, so please read it comfortably: ok_hand:

message_form.rb


class MessageForm

  # part-1
  include ActiveModel::Model
  include Virtus.model
  extend CarrierWave::Mount

  validates :body,   presence: true

  attribute :body, String
  attribute :user_id, Integer

  mount_uploader :picture, PictureUploader
  attr_accessor :pictures

  # part-2
  def initialize(message = Message.new)
    @message = message
    self.attributes = @message.attributes if @message.persisted?
  end

  # part-3
  def assign_attributes(params = {})
    @params = params
    pictures_attributes = params[:pictures_attributes]
    @pictures ||= []
    pictures_attributes&.map do |pictures_attribute|
      picture = Picture.new(pictures_attribute)
      @pictures.push(picture)
    end
    @params.delete(:pictures_attributes)
    @message.assign_attributes(@params) if @message.persisted?
    super(@params)
  end

  # part-4
  def save
    return false if invalid?
    if @message.persisted?
      @message.pictures = pictures if pictures.present?
      @message.save!
    else
      message = Message.new(user_id: user_id,
                            body: body)
      message.pictures = pictures if pictures.present?
      message.save!
    end
  end

end

part-1 (Introduction of module)

python


  include ActiveModel::Model
  include Virtus.model
  extend CarrierWave::Mount

  validates :body, presence: true

  attribute :body, String
  attribute :user_id, Integer

  mount_uploader :picture, PictureUploader
  attr_accessor :pictures

This is a collection of included modules.

-ActiveModel :: Model allows validation to be used when saving a MessageForm object. The validates part. It's convenient to be able to validate without inheriting ActiveRecord.

-Virtus.model defines the attribute name and type of this class. The attribute part. It specifies what to receive as a parameter. By the way, I have installed the gem virtus.

-CarrierWave :: Mount is for uploading images. The part of mount_uploader.

attr_accessor is for setting image information in the message. (Used in part-4.)

part-2 (Initialization of object)

Give the object a property.

python


  def initialize(message = Message.new)
    @message = message
    self.attributes = @message.attributes if @message.persisted?
  end

The point here is the argument of initialize. If the controller's new method has an argument, it is receiving it.

python


  def edit
    @message = MessageForm.new(message = Message.find(params[:id]))
  end

In other words, when the object is created with new, if there is information of the message already created, the attributes of the form object are rewritten based on it. ('self.attributes` part)

part-3 (update object)

I'm sorry for those who are getting tired. A little more patience. .. ..

python


  def assign_attributes(params = {})
    @params = params
    pictures_attributes = params[:pictures_attributes]
    #If you write pictures, you can call up the image information.
    @pictures ||= []
    pictures_attributes&.map do |pictures_attribute|
      picture = Picture.new(pictures_attribute)
      @pictures.push(picture)
    end
    #Remove image information from parameters
    @params.delete(:pictures_attributes)
    #Update the information of the message you have already created
    @message.assign_attributes(@params) if @message.persisted?
    #Update form object information
    super(@params)
  end

Here, assign_attributes sets the parameter value to @ message.

The assign_attributes method is originally a method for updating data,

-I want to reflect the editing information of the form object in the already created message. ・ I want to be able to get or set image information by writing pictures (attr_accessor in part-2)

I've modified it a bit for that reason: sunglasses:

part-4 (conversion work)

Finally the last! It is a conversion work to the Message model.

python


  def save
    #Validate before storage
    return false if invalid?
    #Processing when editing
    if @message.persisted?
      @message.pictures = pictures if pictures.present?
      @message.save!
    else
    #Processing in case of new creation
      message = Message.new(user_id: user_id,
                            body: body)
      message.pictures = pictures if pictures.present?
      message.save!
    end
  end

In the case of editing, it is no different from normal saving. The @ message at this time is the same as the message in part-2.

The important thing is to convert to the Message object below.

python


      message = Message.new(user_id: user_id,
                            body: body)

I'm passing the value that the form object brought to the column. This completes the conversion from the MessageForm to the Message object.

python


      message.pictures = pictures if pictures.present?

Finally, set the image information in the message and finish. Thank you for your hard work: clap:

environment

ruby: 2.7.1 rails: 6.0.3.3

Recommended Posts

Manipulate multiple models on form objects [De-accepts_nested_attributes_for]
Use devise on multiple models
Notes on multiple inheritance