19

I'm relatively new to rails and finally found the right way to use accepts_nested_attributes_for.

However, there are some serious resources on the web who say that using accepts_nested_attributes_for is generally a bad practice (like this one).

What changes are necessary to avoid accepts_nested_attributes_for and in which folder would you put the additional class-file (I guess one needs an additional class).

I read that virtus is appropriate for that. Is that right?

Here is a very basic example still using accepts_nested_attributes_for (find the full example here):

Models

class Person < ActiveRecord::Base

    has_many :phones
    accepts_nested_attributes_for :phones

end

class Phone < ActiveRecord::Base

    belongs_to :person

end

Controller

class PeopleController < ApplicationController

    def new

        @person = Person.new
        @person.phones.new

    end

    def create

        @person = Person.new(person_params)
        @person.save

        redirect_to people_path

    end

    def index

        @people = Person.all

    end

private

    def person_params

        params.require(:person).permit(:name, phones_attributes: [ :id, :number ])

    end

end

View (people/new.html.erb)

<%= form_for @person, do |f| %>
    <p>
        <%= f.label :name %><br />
        <%= f.text_field :name %>
    </p>
    <%= f.fields_for :phones do |builder| %>
    <p>
            <%= builder.label :number %><br />
            <%= builder.text_field :number %>
    </p>
    <% end %>
    <%= f.submit %>
<% end %>

[edit]
Would it be a good idea to use a service object?

speendo
  • 11,483
  • 19
  • 66
  • 100

4 Answers4

16

Your question implies that you believe accepts_nested_attributes functionality to be a bad thing which is totally not the case and works perfectly well.

I'll start by saying that you don't need an alternative to accepts_nested_attributes_for but I'll cover that at the end of this post.

With reference to the link you provide, it cites nothing about why the poster believes accepts_nested_attributes_for should be deprecated at all and just merely states

in my humble opinion, should be deprecated

Nested attributes are an extremely important concept when considering how to capture multiple records related to a parent in a single form which is not just a Ruby on Rails thing but used in most complex web applications for sending data back to the server from the browser regardless of of the languages used to develop the site.

I'm not criticising the article you point to at all. To me it's just pointing out obvious alternatives to filling up a database backed model with lots of code that is not necessarily related to business logic. The specific example used is merely a coding style preference alternative.

When time is money and the pressure is on and one line of code will do the job versus the 22 lines of code shown in the example my preference in most cases (not all cases) is to use one line of code in a model (accepts_nested_attributes_for) to accept nested attributes posted back from a form.

To answer your question properly is impossible as you have not actually stated why YOU think accepts_nested_attributes_for is not good practice however the simplest alternative is to just extract the params hash attributes in your controller action and handle each record individually inside a transaction.

Update - follow up on comment

I think the author of the linked article argues that, following oop-paradigms, every object should only read and write its own data. With accepts_nested_attributes_for, one object however changes some other objects data.

O.K. Lets clear that up. Firstly OO paradigms suggest no such thing. Classes should be discreet but they are allowed to interact with other classes. In fact there would be no point to an OO approach in Ruby if this was the case as EVERYTHING in ruby is a class therefore nothing would be able to talk to anything else. Just imagine what would happen if an object that just happens to be an instance of your controller were not able to interact with models or other controllers?

With accepts_nested_attributes_for, one object however changes some other objects data.

Couple of points on that statement as it's a complex one I'll try to be as brief as possible.

1) Model instances guard the data. In very complex scenarios involving hundreds of tables in any/most other languages (C, Delphi, VB to name a few) a middle tier in a 3 tier solution does just that. In Rails terms a model is a place for business logic and does the job of the middle tier in a 3 tier solution that is normally backed up by stored procedures and views in the RDBMS. Models quite rightly should be able to talk to each other.

2) accepts_nested_attributes_for does not break any OO principles at all. it merely simplifies the amount of code that you would need to write if the method did not exist (as you are finding out). If you accept attributes that are nested inside a params hash for child models all you are doing is allowing the child models to handle that data in the same way that your controller's action would have to do. No business logic is bypassed and you get added benefits.

Lastly

I can afford to care about elegance of code (more than about time)

I can assure you that there is nothing elegant about writing 20 + more lines of code than you need to and adding hundreds of lines more code from a gem where one line of code will do the work for you. As others have stated (including me) accepts_nested_attributes_for is not always an appropriate ActiveRecord method to use and it is a good thing that you are doing by looking at different approaches as ultimately you will be able to make better informed judgements as to when to use built in methods and when to write your own. However I would suggest that to fully understand what is going on (as you state you have the time) you would be better writing your own code to handle form objects and accepts nested attributes alternatives. That way you would find yourself understanding so much more.

Hope that makes sense and good luck with your learning.

UPDATE 2

To finally get to your point and in reference to your own answer plus taking into account of the excellent comments others have made on your own answer form objects backed by the virtus gem is a perfectly reasonable solution especially when dealing with the way data has to be collected. The combination helps to separate the user interface logic from the business logic and so long as you are ultimately passing off the data to the models so that business logic is not bypassed (as you show you are doing exactly this) then you have a great solution.

Just don't rule out accepts_nested_attributes out of hand.

You might also gain some benefit from watching the railscasts by Ryan Bates on form objects.

jamesc
  • 11,236
  • 15
  • 63
  • 98
  • I understand your argument very well and I support it under the circumstances you describe. As I am not a professional developer but a hobbiest, I can afford to care about elegance of code (more than about time). I think the author of the linked article argues that, following oop-paradigms, every object should only read and write its own data. With accepts_nested_attributes_for, one object however changes some other objects data. – speendo Jul 14 '13 at 19:59
  • 3
    Picking the best pattern will end up saving you more time in the long run. Polluting your models with many different concerns and responsibilities will only give you a headache later. Using a Form Object where appropriate is a much better practice than using nested attributes. The key is "where appropriate" - there are circumstances where using nested attributes is the better solution. – Logan Serman Jul 14 '13 at 20:55
  • 1
    Also - why deprecation is maybe a bit extreme, I still think it is better to let new developers (speendo) experiment with alternatives to the "Rails way". Recently it seems that a lot of people have begun to use things such as Form Objects after realizing Rails does not scale (the codebase, not the app itself) with huge applications. Discouraging people from doing this is only going to hurt, and Rails will have a harder time moving forward and maturing as a framework. – Logan Serman Jul 14 '13 at 20:59
  • 1
    @LoganSerman can you specify in which it is appropriate to use nested attributes, from the paradigmatic point of view? – speendo Jul 14 '13 at 21:03
  • It is going to be different in every case. Nested attributes are good for very simple nested models imo. But when you have a large form, with large nested models (or many nested models) then a form object is more appropriate. You just have to experiment with them until you get a feel for when to use which tool. – Logan Serman Jul 14 '13 at 21:28
  • @speendo I have added an update to my answer in the hope that it clarifies things for you. – jamesc Jul 15 '13 at 02:19
  • @jamesw Thanks for your answer! You really make a great effort! I have to add that with "every object should only read and write its own data" I did mean discrete objects (with their own putters and getters) - I didn't think of no communication between objects. You point that out anyway. Coming from Java (the language not the island), it feels natural to have container objects, therefore I like the form object-approach. However, I understand your arguments. Would you refer to my answer below as an alternative for `accepts_nested_attributes_for` is presented there? I want to accept your answer! – speendo Jul 15 '13 at 08:24
  • @speendo Sure. Glad to be of help – jamesc Jul 15 '13 at 13:57
5

It's much easier to use virtus instead of accepts_nested_attributes_for than I thought. The most important requirement was to dare to make things that were not covered in any of the tutorials I read yet.

Step by step:

  1. I added gem 'virtus' to the Gemfile and ran bundle install.
  2. I wrote a file models/contact.rb and wrote the following code:

    class Contact
      include Virtus
    
      extend ActiveModel::Naming
      include ActiveModel::Conversion
      include ActiveModel::Validations
    
      attr_reader :name
      attr_reader :number
    
    
      attribute :name, String
      attribute :number, Integer
    
      def persisted?
        false
      end
    
      def save
        if valid?
          persist!
          true
        else
          false
        end
      end
    
    private
    
      def persist!
        @person = Person.create!(name: name)
        @phones = @person.phones.create!(number: number)
      end
    end
    
  3. Then I ran rails generate controller contacts and filled *models/contacts_controller.rb* with

    class ContactsController < ApplicationController
    
      def new
    
        @contact = Contact.new
    
      end
    
      def create
    
        @contact = Contact.new(contact_params)
        @contact.save
        redirect_to people_path
    
      end
    
      def contact_params
    
        params.require(:contact).permit(:name, :number)
    
      end
    
    end
    
  4. The next step was the view. I created views/contacts/new.html.erb and wrote this basic form

    <%= form_for @contact do |f| %>
      <p>
        <%= f.label :name %><br />
        <%= f.text_field :name %>
      </p>
    
      <p>
        <%= f.label :number %><br />
        <%= f.text_field :number %>
      </p>
    
      <%= f.submit %>
    <% end %>
    
  5. Of course I also needed to add the route resources :contacts

That's it. Maybe it could be done more elegant. Maybe it would also pay to use only the Contacts-class, also for the other CRUD-actions. I didn't try that yet...

You can find all changes here: https://github.com/speendo/PhoneBook/tree/virtus/app/models

speendo
  • 11,483
  • 19
  • 66
  • 100
  • 4
    It is not Virtus which is doing the heavy lifting here. You are just using a Form Object (as described in the blog post you linked). Virtus is simply a library to help out with making Form Objects (among many other things). – Logan Serman Jul 14 '13 at 20:54
  • @LoganSerman of course, but without it it would be more difficult to create the view, right? – speendo Jul 14 '13 at 21:04
  • 1
    It allows you to create your models with a hash of attributes, so that `Contact.new(contact_params)` is possible. It is essentially making normal objects (your `Contact` form object) act more like ActiveRecord. – Logan Serman Jul 14 '13 at 21:25
  • I believe this example was made according to the one from this video: https://www.youtube.com/watch?v=5yX6ADjyqyE#t=1136 – NoDisplayName Feb 10 '15 at 12:02
  • I would recommend putting the persistence of dependent objects in a transaction - the current `persist!` looks like very bad advice. – user3467349 Mar 28 '16 at 12:40
5

So, the accepted answer just says why accepts_nested_attributes_for is often a good solution, but never actually offers a solution on how to do it. And the example in the linked article will run into problems if you want to make your form accept a dynamic number of nested objects. This is the only solution I've found

https://coderwall.com/p/kvsbfa/nested-forms-with-activemodel-model-objects

For posterity this is the basics, but there's a bit more on the site:

class ContactListForm
include ActiveModel::Model

attr_accessor :contacts

def contacts_attributes=(attributes)
  @contacts ||= []
  attributes.each do |i, contact_params|
    @contacts.push(Contact.new(contact_params))
  end
end
end

class ContactsController < ApplicationController
   def new
      @contact_list = ContactListForm.new(contacts: [Contact.new])
    end
  end

and f.fields_for :contacts should behave like an has_many relationship, and easily handled by your form object.

If Contact isn't an AR model you'll need to spoof persisted? as well.

MCB
  • 1,829
  • 1
  • 16
  • 31
-3

Railscasts also has an episode on nested form models: http://railscasts.com/episodes/196-nested-model-form-revised?view=asciicast

As shown in the comments, cocoon gem simplifies a lot: https://github.com/nathanvda/cocoon