I have and object with a nested model. I am currently getting all the nested objects like so:
#no = Parent.find(params[:parent_id]).children
Now, one of these children has an attribute that identifies them as the favorite. How can I get the favorite child from among the children?
In addition, how can I edit the attributes using fields_for for just that single object in the view/update?
I don't know the name of your attribute that identifies the record as the favorite, but let's say it is a boolean named is_favorite. Considering this abose, the following should work:
children = Parent.find(params[:parent_id]).children
#favorited_children = children.where(is_favorite: true) # return 0..N records! not only 0..1 !
To edit its attributes, you can do as following (you will have to translate it in ERB or HAML, depending on what your app uses):
form_for #favorited_children do |form_builder|
form_builder.text_field :name
form_builder.check_box :is_favorite
end
Hope this helps!
You could also look at using an ActiveRecord Association Extension
This basically works by creating instance methods you can chain onto the child association, like so:
#app/models/parent.rb
Class Parent < ActiveRecord::Base
has_many :children do
def favorites
where(is_favorite: true) #-> to use MrYoshi's example
end
end
end
This will allow you to use the following:
#parent = Parent.find params[:id]
#favorites = #parent.children.favorites
Related
I would like to know the best practice when I need a computed attribute that require a call to the database.
If I have a Parent that has many Child, how would I render a children_count attribute in ParentController#index as I don't want to render the children, just the count? what's the best way to do it?
Thank you!
Model:
class Parent < ApplicationRecord
has_many :children
def children_count
children.count # Wouldn't it ask the database when I call this method?
end
end
Controller:
class ParentsController < ApplicationController
def index
parents = Parent.all
render json: parents, only: %i[attr1, attr2] # How do I pass children_count?
end
end
The Rails way to avoid additional database queries in a case like this would be to implement a counter cache.
To do so change
belongs_to :parent
in child.rb to
belongs_to :parent, counter_cache: true
And add an integer column named children_count to your parents database table. When there are already records in your database then you should run something like
Parent.ids.each { |id| Parent.reset_counters(id) }
to fill the children_count with the correct number of existing records (for example in the migration in which you add the new column).
Once these preparations are done, Rails will take care of incrementing and decrementing the count automatically when you add or remove children.
Because the children_count database column is handled like all other attributes you must remove your custom children_count method from your Parent class and can still simple call
<%= parent.children_count %>
in your views. Or you can add it to the list of attributes you want to return as JSON:
render json: parents, only: %i[attr1 attr2 children_count]
children.count will call the database, yes; however, it will do it as a SQL count:
SELECT COUNT(*) FROM "children" WHERE "children"."parent_id" = $1
It doesn't actually load all of the child records. A more efficient method is to use a Rails counter_cache for this specific case: https://guides.rubyonrails.org/association_basics.html#options-for-belongs-to-counter-cache
Here is my setup, followed by an explanation of what I am trying to accomplish.
class Layer < ActiveRecord::Base
has_and_belongs_to_many :components
end
class Component < ActiveRecord::Base
has_and_belongs_to_many :layers
end
class ImageComponent < Component
# I want this table to inherit from the Component table
# I should be able to add image-specific fields to this table
end
class VideoComponent < Component
# I want this table to inherit from the Component table
# I should be able to add video-specific fields to this table
end
What I want to be able to do:
layer.components << ImageComponent.create
layer.components << VideoComponent.create
In practice, I realize that ImageComponent and VideoComponent will actually have to inherit from ActiveRecord::Base. Is there any way to nicely implement model subclassing in Rails?
Right now I have my Component model setup to be polymorphic such that ImageComponent and VideoComponent each has_one :component, as: :componentable. This adds a layer of annoyance and ugliness to my code though:
image_component = ImageComponent.create
component = Component.create
component.componentable = image_component
layer.components << component
I guess a simple way to explain this is that I want to implement a habtm relationship between Layers and Components. I have multiple types of Components (i.e. ImageComponent, VideoComponent) that each have the same base structure but different fields associated with them. Any suggestions on ways this can be accomplished? I feel that I am missing something because my code feels hackish.
The "official" way to achieve this in Rails is to use Single Table Inheritance. Support for STI is built into ActiveRecord: http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance
If you want to use Multi Table Inheritance you would have to implement it by yourself...
here the main issue is between the Component and its types and not Layer and Component. i had a similar problem. will explain the solution specific to ur problem.
Store the type(Image/Video) as resource for Component and have a controller for Component and not all the types()
let the model structure be as
Component < ActiveRecord::Base
accepts_nested_attributes_for :resource
belongs_to :resource, :polymorphic => true, :dependent => :destroy
def resource_attributes=(params = {})
self.resource = spec_type.constantize.new unless self.resource
self.resource.attributes = params.select{|k| self.resource.attribute_names.include?(k) || self.resource.class::ACCESSOR.include?(k.to_sym)}
end
#component will be either image or video and not both
Image < ActiveRecord::Base
has_one :component, as :resource
Video < ActiveRecord::Base
has_one :component, as :resource
and a single controller as ComponentsController for CRUD of Component. Since the Component accepts attributes for resource(ie image/video), u can save the component as well as the resource and add normal validations for each resource.
the basic view for adding a Component can be as
= form_for(#component, :url => components_path, :method => :post) do |f|
= fields of Component
= f.fields_for :resource, build_resource('image') do |image|
= fields of resource Image
= f.fields_for :resource, build_resource('video') do |video|
= fields of resource Video
the fields for Image/Video can be added using the helper method
module ComponentsHelper
def build_resource(klass)
klass = "{klass.capitalize}"
object = eval("#{klass}.new")
if #component.resource.class.name == klass
object = #component.resource
end
return object
end
end
since the Component can have only one related resource(image/video), u need to select the the resource type on the view(in my case it was a dropdown list) and depending upon the selected resource show its fields and hide/remove all other resources fields(if image is selected, remove video fields using javascript). When the form is submitted, the method from Component model filters out all the key-value pairs for the intended resource and creates the component and its related resource.
Also
1) keep the field names for each resource unique cause when the form is submitted, the hidden resource(unwanted resources) fields are submitted which overwrite the intended resource fields.
2) the above model structure gives problem for resource attr_accessor only(they are not accessible on rails console). it can be solved as
ACCESSOR = ['accessor1', 'accessor2'] #needed accessors
has_one :component, :as => :resource
attr_accessor *ACCESSOR
See how to implement jobpost functionality that has 3 fixed categoris
i hope this helps.
With STI, you are sharing the same table with several model classes, so if you want subclassed models to have unique fields (database columns), then they need to be represented in that common table. From the comments in your example, it appears that this is what you want.
There is a trick you can do, however, which involves having a string column in the table that each model can use to store custom serialized data. In order to do this, it has to be OK that these data elements aren't indexed, because you won't be able to easily search on them within SQL. Let's say you call this field aux. Put this in the parent model:
require 'ostruct'
serialize :aux, OpenStruct
Now let's say you want the fields called manager and experience in a subclassed model, but none of the other STI models need this field and you won't need to search based on these attributes. So you can do this in the subclassed model:
# gets the value
def manager
return self.aux.manager
end
# sets the value
def manager=(value)
self.aux.manager = value
end
# gets the value
def experience
return self.aux.experience
end
# sets the value
def experience=(value)
self.aux.experience = value
end
In this example, single table inheritance still works fine and you also get custom persistant attributes for subclassed models. This gives you the benefits of sharing code and database resources among several models, but also allows each model to have unique attributes.
This post seems good for how to create two models with one form. But how would you do it if the two models share one or more of the attributes?
That post seems fairly outdated, I would recommend using accepts_nested_attributes_for and fields_for in your form instead. That said, overlapping attributes should probably be set in your model's callbacks. Say you want a project's name to be automatically set to first task's name.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
before_validation :set_name_from_task
private
def set_name_from_task
self.name = tasks.first.name
end
end
If your 2 models are completely unrelated, you can assign certain params to them directly in the controller.
def create
#foo = Foo.new(params[:foo])
#bar = Bar.new(params[:bar])
#bar.common_attr = params[:foo][:common_attr]
# validation/saving logic
end
Although this is not a great practice, this logic should ideally be moved into models.
Relating to my last question here: Rails: Finding all associated objects to a parent object
Is it possible to sort multiple separate child objects in Rails by creation date, and then list them? Using the previous example I have a resume with two different has_many child objects, I would like to fetch them and then sort them based on creation date and then use that to display them.
I assume that you have two (or more) seperate models for children objects, so your Parent model looks like this:
class Parent < ActiveRecord::Base
has_many :dogs
has_many :cats
end
To sort them and get them generally as children you can write method (similar to #gertas answer):
def children
#children ||= (self.dogs.all + self.cats.all).sort(&:created_at)
end
and put it in Parent model. Then you can use it in controller:
#parent = Parent.find(params[:id])
#children = #parent.children
Now we'll try to display them in a view. I assume that you have created two partials for each model _cat.html.erb and _dog.html.erb. In view:
<h1>Children list:</h1>
<% #parent.children.each do |child| %>
<%= render child %>
<% end %>
It should automaticaly find which partial should be used, but it can be used only if you follow Rails way. If you want to name partials in different way, or store it in different directory, then you would have to write your own methods that will choose correct partial based on type od object.
You can add an accessor method on your parent model:
class Parent < ActiveRecord::Base
def sorted_children
children.scoped( :order => 'created_at DESC' )
# or, in rails3:
# children.order('created_at DESC')
end
end
If the natural order for your child model is the date field and you would like to do that everywhere, then just set a default scope on it:
class Child < ActiveRecord::Base
default_scope :order => 'created_at DESC'
end
As child objects are in different types and they are fetched separately (separate has_many) you have to do sorting in Ruby:
sorted_childs=(#resume.child1_sections.all + #resume.child2_sections.all).sort(&:created_at)
Otherwise you would need to introduce table inheritance with common columns in parent. Then it would be possible to have another has_many for all children with :order.
I am trying to save to a join table in a habtm relationship, but I am having problems.
From my view, I pass in a group id with:
<%= link_to "Create New User", new_user_url(:group => 1) %>
# User model (user.rb)
class User < ActiveRecord::Base
has_and_belongs_to_many :user_groups
accepts_nested_attributes_for :user_groups
end
# UserGroups model (user_groups.rb)
class UserGroup < ActiveRecord::Base
has_and_belongs_to_many :users
end
# users_controller.rb
def new
#user = User.new(:user_group_ids => params[:group])
end
in the new user view, i have access to the User.user_groups object, however when i submit the form, not only does it not save into my join table (user_groups_users), but the object is no longer there. all the other objects & attributes of my User object are persistent except for the user group.
i just started learning rails, so maybe i am missing something conceptually here, but i have been really struggling with this.
Instead of using accepts_nested_attributes_for, have you considered just adding the user to the group in your controller? That way you don't need to pass user_group_id back and forth.
In users_controller.rb:
def create
#user = User.new params[:user]
#user.user_groups << UserGroup.find(group_id_you_wanted)
end
This way you'll also stop people from doctoring the form and adding themselves to whichever group they wanted.
What does your create method look like in users_controller.rb?
If you're using the fields_for construct in your view, for example:
<% user_form.fields_for :user_groups do |user_groups_form| %>
You should be able to just pass the params[:user] (or whatever it is) to User.new() and it will handle the nested attributes.
Expanding on #jimworm 's answer:
groups_hash = params[:user].delete(:groups_attributes)
group_ids = groups_hash.values.select{|h|h["_destroy"]=="false"}.collect{|h|h["group_id"]}
That way, you've yanked the hash out of the params hash and collected the ids only. Now you can save the user separately, like:
#user.update_attributes(params[:user])
and add/remove his group ids separately in one line:
# The next line will add or remove items associated with those IDs as needed
# (part of the habtm parcel)
#user.group_ids = group_ids