How can I put model associations calls from views to controllers in Rails 4 or later?
For example I have:
Model:
class Parent
has_many :children
end
class Child
end
Controller:
class ParentController < ApplicationController
def index
#parents = Parent.all
end
end
class ChildrenController < ApplicationController
def index
#children = Parent.find(params[:parent_id]).children
end
end
View:
parents/index.html.erb
<% #parents.each do |parent| %>
<% render parent.children %>
<% end %>
some partial children/_child.html.erb
<%= child.name %>
route.rb
resources :parents do
resources :children
end
How can I substitute call of method parent.children by using somehow logic in controller - for example, ChildrenController::index method that requires url parameter?
I believe this will allow me to abstract view from model.
By abstraction I mean - if i change the model - for example, it won't have has_many association between parent and child - but still want to preserve the view then i will have only change the code in controller not in the view.
Further, i can extract the model logic in controller to some business class that will act like a model interface to use.
I am trying to build two-layered architecture with separated Presentation Layer (views, controllers) and Business layer (business classes, activerecords).
Probably this architecture could avoid the cases like with futurelearn portal that decided to split their STI model (single table) to several different ones. But having no real separation it became a creative task of tricking the Rails (see https://about.futurelearn.com/blog/refactoring-rails-sti). Code had a lot of association calls and it was necessary to develop smth to preserve these calls (only because otherwise it would be necessary to change too much code) and change model at the same time.
Not sure what you mean by
I believe this will allow me to abstract view from model.
You are not using model in the view per se. You are using the ActiveRecord associations in the view which I think is the right way to do this sort of thing. Not sure if rendering all the children for each parent is the right thing to do as far as reading all those records in memory is concerned but that's a whole different topic.
FWIW, both parent.children and Parent.find(params[:parent_id]).children will generate the exact same SQL on the backend.
That is ok, if you have such a call in your view, i believe that there is no good way to remove it. The only thing you can do - you can try to hide this call, but this will not bring you real abstraction.
But there are some things, that you need to do:
1. Add includes to load your associations in a single query
def index
#parents = Parent.includes(:children).all
end
2. If you don't need your parents in your view (it's not clear - do you only render children partial, or there are other lines in that view?), only children - you can just load your childrens :)
#children = Children.all
# or, if your children can be without parents
#children = Children.where.not(parent_id: nil).all
I think you missed put belongs_to:
class Parent
has_many :children
end
class Child
belongs_to :parent
end
Related
I'm in the process of learning Ruby on Rails, and now I have created the mobile version of my application.
I created the relation between models ans controller is one-one. Now I want to make changes to manage three models from one controller. I have read and watch videos a lot about how to do this but, it doesn't work when I try to do it in my application.
Models:
class Subject < ActiveRecord::Base
has_many :pages
class Page < ActiveRecord::Base
belongs_to :subject
has_many :sections
class Section < ActiveRecord::Base
belongs_to :page
Controller:
class SubjectsController < ApplicationController
has_mobile_fu
layout "admin"
before_action :confirm_logged_in
def index
#subjects = Subject.newest_first
#pages = #subjects.pages.sorted
end
This is the error:
NoMethodError (undefined method pages' for # <ActiveRecord::Relation::ActiveRecord_Relation_Subject:0x007fbbf3c9b218>):
app/controllers/subjects_controller.rb:10:inindex'
The application works well if I keep each model managed by its controller. The problem started now that I want to control multiple models from one controller.
Can definitely use multiple models in a single controller. The issue here is you're calling a method that doesnt exist for the active record relation.
An active record relation is typically a collection of returned objects from a query using active record. So the newest_first is returning multiple, not just one. If you want to get all pages for the subjects and sort them, you can do this:
#subjects = Subject.newest_first
#pages = #subjects.map(&:pages).flatten.sort { |a, b| a.title <=> b.title }
Can switch the attribute on which you wish to sort by. The map function goes through each one, and returns the object of which i passed in the symbol. It's a shortcut for:
#subjects.map { |subject| subject.pages }
The flatten then takes that array of active record relations and flattens it into a single array. I then just use the array sort.
Edit Here's a way you can do it using the database:
#subjects = Subject.newest_first
#pages = Page.where.not(:subject_id => nil).order(:title)
MVC
Something else you'll benefit from is to look at the MVC Programming Pattern:
Rails is famous for its strict coherence to the Model-View-Controller pattern, as it works like this:
You send a request to your app
Rails "routes" your request to a specific controller / action
The controller will then collate data from your Models
The controller will then render a view to display this data
The relationship between models and controllers is exclusive; meaning you don't have to call certain models from a controller, etc.
So the basic answer is no, you don't need to call a single model from a controller. However, you do need to ensure you have the correct model associations set up, as per the explanation below:
Associations
The caveat here, is that since Ruby is object-orientated (and Rails, by virtue of being built on Ruby, also being so), it's generally considered best practice to build your application around objects
"Objects" are basically elaborate variables (constructed from your Model classes), but the pattern behind making OOP work properly is super important - everything from Rails' routes to your controller actions are designed to be object-ORIENTATED
Each time you initiate an instance of a Model, Rails is actually building an object for you to use. This object allows you to call / use a series of attributes / methods for the object, allowing you to create the experience you require with Rails
--
The bottom line -
I would highly recommend examining the ActiveRecord Associations in your models (which will determine whether you need to call a single model or not):
#app/controllers/subjects_controller.rb
Class SubjectsController < ApplicationController
def index
#subjects = Subject.newest_first #-> good use of OOP
#posts = # this is where your error occurs (`.posts` is only an attribute of each `Subject` object instance, which is fixed using the accepted answer)
end
end
Hopefully this gives you some more ideas about how to construct Rails applications
I have a model for Contracts that has a field called "totalAward". In my index.html.erb view I have the following code:
<p> Total Award Amount: <td><%= number_to_currency(Contract.sum('awardAmount')) %></td> </p>
I'm fairly certain it's not a best practice for me to do a database call in the view, but I am not sure how to do it in the controller or the model in a way that I can render it in the view. Could someone help me with how make that database call in the controller or the model?
Thanks!
The instance variable (#contracts) from your controller will give you access to the attributes of your model. I believe you want to use ActiveRecord's sum method. Here is how you can use it in your view:
<%= #contracts.sum(:totalAward) %>
This way, you are not querying the db in your views. but you have access to the instance variable (#contracts) in your controller which holds a collection of all of your contracts.
Further to Wali Ali, you will be best sticking to the MVC programming pattern (the basis of Rails). This means you need to keep your data-allocation (set variables) in your controller, and data storage in your models
I would use a instance method:
#app/models/contract.rb
Class Contract < ActiveRecord::Base
def total
sum(:totalAward)
end
end
This will allow you to perform things like this:
#app/controllers/contracts_controller.rb
Class ContractsController < ApplicationController
def index
#contracts = Contract.all #-> #contracts.total for all
#contracts = Contract.where(some: value) #-> #contracts.total for these items
end
end
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 have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.