Confused about using or not using nested attributes - ruby-on-rails

I would like to know the use cases for nested attributes. The pros and cons of using vs. not using it.
I have a model that has a lot of has_many associations. Example:
class Post
has_many :visitors
has_many :pageviews
has_one :metric
end
Although I like the idea of just sending one request and having all of those things created with the correct associations, I don't like the idea that all the creation of the visitors, pageviews and metric are in the PostsController. I very much like the separation of concerns. Is there any clear rule that I should follow when dealing with nested attributes?
Thank you.

In data-modelling we sometimes split up stuff over different tables/models, and imho nested models/nested forms are mostly used where the nested model has no reason to exist without the parent and vice versa. E.g. a person with their addresses: when creating a person we immediately need to add their address because (for instance in a delivery situation) a person without an address makes no sense.
Nested forms, where we can edit parent/child models as a whole, was popular and imho still has its benefit/place in some very specific situations (like the aforementioned example).
But in most cases, where the creation of the parent is not dependant on the child, I rather prefer to use ajax calls. I present all the information on a page, but when adding a child I make sure it is immediately saved and stored in the database (using their own controller --while visiting the parent's show page for instance).
I believe in most cases that the default/standard mega form is not the best UI/UX solution, and I believe that should be the main driver how to build your pages.
Coming back to your example: these should never be shown in one huge form, nobody is going to edit a post, and the stats, right? Statistics are collected and shown, but hopefully never "created". Normally one tracks actual pageviews, visitors ...

I think you're making a leap you don't need to.
I don't like the idea that all the creation of the visitors, pageviews and metric are in the PostsController.
They shouldn't be and don't have to be. In fact, they never are by default.
Run rails routes in your console and you'll see each of those has_many models have their own controllers and views.
It is very rare I have a web app with a model that doesn't have and use its own controller.
A route like /posts/:id/visitors should point to visitors#index, not something like posts#visitors
If you're putting everything into one controller, I'd argue you aren't actually nesting, you're expanding.
And I'd argue the point of relational databases is to have relationships, so limiting your relationships seems kinda self-limiting.

I don't think you have really grasped what nested attributes is used for in the first place. Its used when you need to CRUD a resource and its children in a single request.
Its use is really dictated by the user experience requirements. Sometimes you actually need a single form like this very common order form example:
class Order
has_many :line_items
accepts_nested_attributes_for :line_items
end
class LineItem
belongs_to :product
end
<%= form_for(#order) do |f| %>
<%= f.fields_for(:line_items) do |ff| %>
<%= ff.number_field :quantity %>
<%= ff.collection_select :product_id, Product.all, :id, :name %>
<% end %>
<% end %>
class OrdersController < ApplicationController
# ...
def update
#order = Order.find(params[:id])
if #order.update(order_params)
# ..
else
# ...
end
end
private
def order_params
params.require(:order)
.permit(:line_items_attributes: [:quantity, :product_id, :_destroy])
end
end
This is just that common checkout form where users can change the number of products in their cart. It lets the users manipulate multiple records at once in a plain old synchronous form.
That said nested attributes is probably one of the most misused components of Rails though and everything beyond 1 level of nesting usually ends up in a hideous mess. Its also a very common misconception that it should be used to assign associations which should in most cases just by done by adding selects or checkboxes that point to the _id or _ids attributes created by the associations.
If you are using it just to mosh everything into a single controller because "I don't want to have too many classes. Waaah" then yes its a huge anti-pattern.
The alternative really is using AJAX to let the user CRUD child records without reloading the page.
I would say that your example is not a good candidate for nested attributes. Are any of those associations actually even created by the user?

Related

Ruby on rails - nested forms with has_many through

Basically, I want to write a web-based curation tool for clinical conditions (disease) and their underlying genetics. Say, I have a clinical condition (class: Phenotype), and I have "mutations"(class: Genotype) that belong to that condition - either individually (i.e. a given mutation is directly causing this condition) or as a group (two or more mutations together cause the condition). Each condition can have one or more of such groups (i.e. may be cause by different mutations or groups of mutations). So I figured I need to create a grouping class (class: GenotypeGroup) to make that association. What I cannot figure out is how to do the form... First, I would want to enter a phenotype with some description. I then would like to use the "show" view to add a new genotype_group to that phenotype (Add new genotype group). This would have to create, implicitly (since it is basically only a cross-reference table) the genotype_group and one or more genotypes which it links to the phenotype entry.
Right now, I have:
class Phenotype
has_many :genotype_groups
has_many :genotypes, through: :genotype_groups
accepts_nested_attributes_for :genotype_groups
end
class GenotypeGroup
belongs_to :phenotype
has_many :genotypes
accepts_nested_attributes_for :genotypes
end
class Genotype
belongs_to :genotype
end
And zero idea how this would work in terms of nested forms. If anyone has a helpful web resources (been googling for > 1hour now, but apparently don't even know what the thing I am trying to do is called..) - that would be great!
Cheers,
M
Turns out I was missing two things:
a) When nesting the genotype object, I need to add a "Genotype.new" to the nested form element:
<% f.fields_for :genotypes, Genotype.new do |gt| %>
something_here
<% end %>
b) I had to declare which variables should be carried (i.e. are permitted) by the params object in the respective controller(s) so that I could pass those values between the classes during the nested object creation.

Rails API: One Request, Multiple Controller Actions

I have multiple models that in practice are created and deleted together.
Basically I have an Article model and an Authorship model. Authorships link the many to many relation between Users and Articles. When an Article is created, the corresponding Authorships are also created. Right now, this is being achieved by POSTing multiple times.
However, say only part of my request works. For instance, I'm on bad wifi and only the create article request makes it through. Then my data is in a malformed half created, half not state.
To solve this, I want to send all the data at once, then have Rails split up the data into the corresponding controllers. I've thought of a couple ways to do this. The first way is having controllers handle each request in turn, sort of chaining them together. This would require the controllers to call the next one in the chain. However, this seems sorta rigid because if I decide to compose the controllers in a different way, I'll have to actually modify the controller code itself.
The second way splits up the data first, then calls the controller actions with each bit of data. This way seems more clean to me, but it requires some logic either in the routing or in a layer independent of the controllers. I'm not really clear where this logic should go (another controller? Router? Middleware?)
Has anybody had experience with either method? Is there an even better way?
Thanks,
Nicholas
Typically you want to do stuff like this -- creating associated records on object creation -- all in the same transaction. I would definitely not consider breaking up the creation of an Authorship and Article if creating an Authorship is automatic on Article creation. You want a single request that takes in all needed parameters to create an Article and its associated Authorship, then you create both in the same transaction. One way would be to do something like this in the controller:
class Authorship
belongs_to :user
belongs_to :article
end
class Article
has_many :authorships
has_many :users, through: :authorships
end
class ArticlesController
def create
#article = Article.new({title: params[:title], stuff: [:stuff]...})
#article.authorships.build(article: #article, user_id: params[:user_id])
if #article.save
then do stuff...
end
end
end
This way when you hit #article.save, the processing of both the Article and the Authorship are part of the same transaction. So if something fails anywhere, then the whole thing fails, and you don't end up with stray/disparate/inconsistent data.
If you want to assign multiple authorships on the endpoint (i.e. you take in multiple user id params) then the last bit could become something like:
class ArticlesController
def create
#article = Article.new({title: params[:title], stuff: [:stuff]...})
params[:user_ids].each do |id|
#article.authorships.build(article: #article, user_id: id)
end
if #article.save
then do stuff...
end
end
end
You can also offload this kind of associated object creation into the model via a virtual attribute and a before_save or before_create callback, which would also be transactional. But the above idiom seems more typical.
I would handle this in the model with one request. If you have a has_many relationship between Article and Author, you may be able to use accept_nested_attributes_for on your Article model. Then you can pass Authorship attributes along with your Article attributes in one request.
I have not seen your code, but you can do something like this:
model/article.rb
class Article < ActiveRecord::Base
has_many :authors, through: :authorship # you may also need a class_name: param
accepts_nested_attributes_for: :authors
end
You can then pass Author attributes to the Article model and Rails will create/update the Authors as required.
Here is a good blog post on accepts_nested_attributes_for. You can read about it in the official Rails documentation.
I would recommend taking advantage of nested attributes and the association methods Rails gives you to handle of this with one web request inside one controller action.

How to add multiple records to a category

I'm not sure the best way to ask this question, and I realize it may be out of the scope of this. I am attempting to learn rails by making simple apps that I could potentially use one day. I realize these aren't robust or secure apps, but making something I could use gives me some motivation. I've begun to get out of scope of the simple "create a blog/twitter" phase and can't find much help.
I'm attempting to make an app to book outdoor trips.
Models: Leaders, Groups, Trips, Activities, Locations and Plans
The idea is to create a "plan" to send to someone that is a publicly viewable grouping of trips. I've got everything in place to manipulate everything by the plans. They are all straightfoward models and relations.
I'm getting hung up on the best way to create a plan, and add multiple, existing trips to it. Each trip has a plan_id which can be set and the plan can simply pull that collection, but I don't know how to best (and most simply - without javascript if possible) show a list of trips and be able to select multiple and add them to a plan.
Does this make sense? I think the easiest way to begin to unravel it would be to check out the git repo: https://github.com/ryanmccrary/cabra
The https://github.com/ryanmccrary/cabra/tree/trip-plan-add branch is a half-baked attempt at one method, but I think I went about it the wrong way.
I'm not looking for the "solution" as much as the best way to do something like this and possibly some hints to get me started...
Have you considered using simple_form?
Creating complex forms at the beginning of learning Rails can be a major setback, with simple form, you can just do this and trips that are already created will apear.
<%= simple_form_for #plan do |f| %>
<%= f.input :name %>
<%= f.association :trips %>
<%= f.button :submit %>
<% end %>
simple_form can and will make you do certain things and html markup a certain way which will make it less customizable, but if you are just trying to make simple has_many relationships, this would be the fastest way to go.
Hope it helps, I know how frustrating it can be to learn a framework and get stuck on something that seems to be trivial at first but turns out to be something that ruins the whole week.
Should try looking at some railscast episodes, there haven't been updates for a while, but there are very specific topics with source code.
Added:
If you want to associate existing trips to your plan, you would also need to make the associations between them a has many to many. You would need to create another table that would hold the association between the two together. lets call this an "agenda", sorry can't think of a good word for it. For more about associations, consult the RailsGuides.
models/trip.rb
class Trip < ActiveRecord::Base
has_many agendas
has_many plans, through: agenda
end
models/plan.rb
class Plan < ActiveRecord::Base
has_many agendas
has_many trips, through: agenda
end
models/agenda.rb
class Agenda < ActiveRecord::Base
belongs_to plan
belongs_to trip
end

Capturing current_user.id saving deeply nested records

Lets say I have a working form that looks like the following
=form_for #survey do |f|
=f.text_field :name
=f.fields_for :questions do |question_fields|
=question_fields.text_field :question_text
=question_fields.fields_for :answers do |answer_fields|
=answer_fields.text_field :answer_text
Because different parts of the form can be added and updated by different users I need a way to get the user_id into each model before it is saved. I realize it is not mvc compliant to be able to access current_user inside the model, that being said I am left without a solution.
If I was only saving one object it would be simple enough to assign the current_user.id to the object in the controller, but given the deeply nested nature of this form that starts to look like an ugly solution.
Is there an expert/railsy way to handle this?
Rails 3.2, devise
Can't each of the objects simply steal the user_id from their "parent" relationship? This is a common pattern:
class Answer < ActiveRecord::Base
before_validation :assign_user_id
protected
def assign_user_id
# Don't attempt if the question is not defined,
# or the user_id field is already populated.
return unless (self.question or self.user)
self.user_id = self.question.user_id
end
end
This involves a bit of additional database activity to resolve the answer for each question, as creating it in a scope is not sufficient, but it makes it pretty much fool-proof.
What you probably want to do is stuff in the user_id parameter when creating each record. This means your create call needs to merge in a :user_id key where required. The nested helper doesn't do this by default, though, so if you're using that you may just leave it up to the assign method.

Formtastic and Formtaghelper

I'd like to create a user registration form where the user ticks some boxes that do not connect to the model.
For example, there might be a 'terms & conditions' box that I don't want to have a boolean field in the User model saying 'ticked Terms & Conditions'. Instead I want to create a record somewhere else (like a transaction) that recorded the date/time they accepted the T&Cs.
Another example might be some preference they indicated that I'll use later and hold in the session for now, like 'remember me'.
I can mix these types of fields with the regular form helper. How could I do either one of the examples above when using formtastic? It kind of sticks to have to mix traditional rails tags with lovely clean formtastic code.
You can create any number of virtual attributes in your model that do not necessarily need to be tied to a database column. Adding attr_accessor :terms_and_conditions to your user model will make this 'field' available to formtastic -- even though it's not a database field. You can validate it like any other field or create your own setter method to create a record elsewhere if that's what you need.
I'm inclined to disagree with the approach to use attr_accessors for action-specific entry elements. If Ts&Cs need to be recorded then that makes sense, but sometimes you need data that really is unrelated to the model and is only related to the specific action at hand, such as 'perform some heavyweight operation when executing the action'.
Lets say you have a sign-up form, and you're not using OAuth, and you have an option to specify twitter username and password on sign up. This is fine:
<%= form.input :twitter_username %>
<%= form.input :twitter_password, :as => :password %>
But this bit below confuses me -- its like formtastic in this case is actually taking away what is already there. Is there a way of adding params[:your-object] while still getting formastic to do all its lovely layout stuff?
How about:
class User < ActiveRecord::Base
...
#I don't want this here. Its only for UserController#create.
#attr_accessor :tweet_when_signed_up
...
end
and:
<%= form.input :tweet_when_signed_up, :as => :checkbox, :param_only => true %>
param_only is my made-up suggestion. It says 'this isn't even a transient property. Its just for this action.
class UserController < ActionController::Base
...
def create
if params[:tweet_when_signed_up] # haven't done this yet -- == 1 or !.nil?
Tweeter.tweet( ... )
end
#user = User.create( params[:user] )
end
The ability to do this is probably there -- does anyone know how to do effectively what I think is a good idea above?
thanks!
Instead I want to create a record
somewhere else (like a transaction)
that recorded the date/time they
accepted the T&Cs.
Use the attr_accessor that bensie describes to integrate the field with formtastic.
Formtastic is about view logic, while the relationship are more model logic. Don't worry about creating the related record in the form. Instead, use callbacks like before_update and after_save in the model to ensure the related record has been created.

Resources