I have the following relationships modeled in a Rails3 application:
class User < ActiveRecord::Base
has_and_belongs_to_many :skills
end
class SkillsUser < ActiveRecord::Base
end
class Skill < ActiveRecord::Base
has_and_belongs_to_many :users
end
The "SkillsUser" model represents the many-to-many association between users and skills. In this way, when a User adds a new Skill, and said Skill already exists in the "skills" table (i.e "Java"), I simply create the relationship between the existing skill and the user in the skills_users table. All good.
Within the User's view, I display a list of Skills. And I have a fragment caching block wrapped around those skills.
<% cache([user,"skills"]) do %>
<div id="skills-grid">
<% user.sorted_skills.each do |s| %>
...
<% end %>
</div>
<% end %>
On a separate edit page, a User can add or delete a Skill. This action simply creates or removes a skills_users record. And when this happens, I need to invalidate the fragment cache so that the skills render appropriately on the User view.
So I created a CacheSweeper who's purpose in life is to observe the skills_users relationship. Here's the controller:
class SkillsController < ApplicationController
autocomplete :skill, :name
cache_sweeper :skill_user_sweeper
def create
#user = User.find(params[:user_id])
#Make sure the current user has access to
#associate a skill to the user in the request
if(#user.id = current_user.id)
SkillsHelper.associate_skill(#user,params[:skill][:name])
#skill = Skill.find_by_name(params[:skill][:name])
end
respond_to do |format|
format.js
end
end
def destroy
#skill = Skill.find_by_id(params[:id])
#user = User.find_by_id(params[:user_id])
#Destroy the relationship, not the skill
#user.skills.delete(#skill) if(#skill.can_be_tweaked_by?(current_user))
respond_to do |format|
format.js
end
end
end
And here's the sweeper:
class SkillUserSweeper < ActionController::Caching::Sweeper
observe SkillsUser
def after_create(skill_user)
expire_cache_for(skill_user)
end
def after_update(skill_user)
expire_cache_for(skill_user)
end
def after_destroy(skill_user)
expire_cache_for(skill_user)
end
private
def expire_cache_for(skill_user)
expire_fragment([skill_user.user,"skills"])
end
end
The problem is, after adding or removing a skills_users record (after "create" or "destroy" on the SkillsController), the sweeper is never invoked. I have other sweepers working within my project, but none of them observe many-to-many associations.
My question, then, is how does one create a CacheSweeper to observe a "has_and_belongs_to_many" association?
I would try using the user.id rather than user as the key. i.e. change
<% cache([user,"skills"]) do %>
to
<% cache([user.id,"skills"]) do %>
I would also add logger messages inside the callbacks as well as a logger message in the SkillUserSweeper class to make sure it is being loaded.
Related
Rails 5.
New app, all default.
Bullet gem tell me this:
user: root
/children/2
N+1 Query detected
Child => [:parent]
Add to your finder: :includes => [:parent]
N+1 Query method call stack
app/controllers/children_controller.rb:14:in `show'
app/controllers/children_controller.rb:14:in `show'
I have these models:
class Parent < ApplicationRecord
has_many :children
end
class Child < ApplicationRecord
belongs_to :parent
end
I have this controller in children_controller.rb with:
...
def show
#parent = #child.parent
end
...
In my views views/children/show.html.erb I have this:
...
<%= #parent.name %>
...
If I invert this and in view I put:
<%= #child.parent.name %>
and in controller:
...
def show
#nothing more
end
...
I have the same error from Bullet but in html.
How to fix this? Is really a N+1 problem or Bullet is wrong?
The project is really really new. First models.
I don't think its a N+1 problem as a child has only one parent. However, you can use includes, if that makes a difference:
#child = Child.includes(:parent).find(1)
But if you try both on terminal, you will see that ActiveRecord generates 2 SQL statements with and without the includes.
Context is a check-out process, where a user can pick from her/his shipping addresses. Thus a controller action states:
#shipping_addresses = ShippingAddress.where(['user_id = ?', current_user.id])
I find it more appealing to have a boolean value for preferred so that when the check-out process is repeated by the user, s/he is presented with the preferred address. But the question lies in changing this preferred attribute.
In short, a radio button needs to be created to set preferred to true in a form (is there anyway to have on selected, deslecting the other ones?) and then the form needs to update records
def update_preferred
respond_to do |format|
if #shipping_addresses.save
#shipping_addresses = ShippingAddress.where(['user_id = ?', current_user.id]).update_all("preferred = 'false'")
-- selected_shipping_address.update("preferred = 'true'")
end
..also having a hard time with the syntax for declaring the form (as I've never steered away from the standard rails conventions...):
<%= form_for(#shipping_addresses) do |f| %>
One working solution. In the form, include the radio button, which allows only one selection to the user:
<%= radio_button_tag "shipping_address_id", shipping_address.id %>
<%= submit_tag "update" %>
with the defined route to the action, include the method in the controller, which first sets all the found set to false, then the selected shipping_address to true.
def update_preferred
#shipping_address = ShippingAddress.find(params[:shipping_address_id])
#shipping_addresses = ShippingAddress.where(['user_id = ?', current_user.id]).update_all("preferred = 'false'")
#shipping_address.update_column(:preferred, "true")
respond_to do |format|
if #shipping_address.update_attributes(params[:shipping_address])
format.html { redirect_to cart_url(session[:cart_id]), notice: 'Shipping address was successfully updated.' }
else
format.html { redirect_to cart_url(session[:cart_id]), notice: 'Shipping address was not updated.' }
end
end
end
Maybe this can be simplified more. Comments welcome.
I'm a real beginner with MongoDB and MongoID.
I created two scaffolds
class Objet
include Mongoid::Document
field :nom, type: String
embeds_one :coordonnee
end
And
class Coordonnee
include Mongoid::Document
field :adresse1, type: String
field :adresse2, type: String
field :code_postal, type: String
field :ville, type: String
embedded_in :objet
end
That's what I get when creating a new Objet :
Now, I'm trying to show only the field adresse1 for this document, but it doesn't work. I can display only the whole embedded document doing this :
When I do :
<%= #objet.coordonnees.adresse1 %>
I get this error :
undefined method `adresse1' for #<Hash:0x2b2b1f0>
How can I do that ?
EDIT
Doing that, I can display all the elements "Adresse1, adresse2, ville, code_postal" :
Controller
def show
#objet = Objet.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #objet }
end
end
View
<%= #objet.nom %>
<% #objet.coordonnee.each do |t| %>
<%= t[1] %>
<% end %>
But my question is : How to display ONLY one of them ? Such as ville, or code_postal or adresse1... ?
What was your code that works for the full document? It was dropped from your post.
In the mongo Shell, you could do this with dot notation db.collection.find({},{'coordonnees.adresse1':1,'_id':0}) You need to specify the '_id':0 because _id is always returned by default.
The other answer will not work because adresse1 is a subdocument. You must include the reference to coordonnees.
Not hugely familiar with MongoID, but assuming you can make calls straight to mongo, there is a second implicit parameter to all find-like statements called a projection that specifies what exactly you would like to return.
For instance, showing only adresse1 for all items in your collection:
db.collection.find({},{"coordonnees.adresse1": 1, "_id":0})
should return only the adresse1 parameter. I wasn't quite able to tell exactly what context you're displaying the objects in, but regardless of context, api calls to mongo should be fairly straightforward to make. Let me know if I've misinterpreted this question though.
In your posted example, you should change your find function to something like the following:
Objet.find({params[:id]}, {:fields => [coordonnees.adresse1]})
Hope that helps.
I found the solution to my problem.
To display only one element of the hash, I can do :
<%= #objet.coordonnees['adresse1'] %>
I am not sure if you are using embeds_one or embeds_many as you are using singular and plural forms of the relation name interchangeably in your question.
If it is a embeds_one the problem is that you should not iterate on #objet.coordonnee as it is a single document. Your view code should look like:
<%= #objet.nom %>
<%= #objet.coordonnee.address1 %>
If it is a embeds_many, your relation name should be plural, then you should be able to use t.address1 in your view.
# model Objet
embeds_many :coordonnees
# view
<%= #objet.nom %>
<% #objet.coordonnees.each do |t| %>
<%= t.address1 %>
<% end %>
How would you set up a Russian doll like key based cache expiration with embedded documents?
As described by 37 signals
I believe touch was added for belongs_to in Mongoid 3.0 but how would you deal with it for embedded documents?
Example classes:
class House
embeds_many :persons
end
class Person
embedded_in :house
end
View:
<% cache ['v1', house] do %>
<%= house.some_attribute %>
<% house.persons.each |person| %>
<% cache ['v1' person] do %>
<%= render 'houses/person', person: person %>
<% end %>
<% end %>
<% end %>
What would be the simplest way to generalize the touching? So that when I update a person, the house it's embedded in gets touched.
EDIT: Or maybe the thinking here is that it's relatively cheap to re-render all the embedded items? Of course I could just do this:
class Person
after_save :touch_house
def touch_house
house.touch
end
end
I implement daisy chaining of embedded touching with observers.
class PersonObserver < Mongoid::Observer
def sweep(person)
person.house.touch
end
alias_method :after_update, :sweep
alias_method :after_create, :sweep
end
When you update or create a person, it touches that person's house effectively updating the houses' update_at time stamp.
In order to use observers, add this to your application.rb:
config.mongoid.observers = :person_observer
I define this concern:
module ParentTouchable
extend ActiveSupport::Concern
def touch_parent
self._parent.touch
end
end
and then I include it in the embedded model, so I can call touch_parent in an after_save callback. Let's say my embedded model is Comment:
class Comment
include Mongoid::Document
include ParentTouchable
after_save :touch_parent
end
My problem is as follows:
I've got a form view, which needs to display success and failure icons after submit.
Before submit it just needs to show the form without the success and failure icons.
We can do this in several ways when this is the form:
<%= form_for #resource do |f| %>
<div class='<%= set_class #resource, :name %>'>
Name: <%= f.text_field :name %>
</div>
<% end %>
Check if the request is a POST:
def set_class( record, attribute )
if request.post?
if record.errors[attribute].any?
return "FAILED"
else
return "SUCCESS"
end
end
# If not submitted, we don't want a class
end
Set a flag after validation ( We can replace request.post? in above solution with record.tried_to_validate ):
class MyModel < ActiveRecord::Base
after_validation :set_tried_to_validate
attr_accessor :validated
def set_validated
#tried_to_validate = true
end
end
But I don't really like these solutions..
Isn't there an inside Rails method to check if the validation process is done?
You can first test for validity..
#form.valid?
Which will generate errors stored in 'errors' on your #form. To see if errors exist on a specific field,
#form.errors[:some_field]
On your form, you can simply do:
<% if #form.errors[:some_field].empty? %>
Valid
<% end %>
As long as some fields generate errors, the whole form will be !valid?, so you'll revert to showing the form again (:new), and you can should 'Valid' or checkmark.
I think you are looking for something like client side validations, if want the validation to show inline on the form. http://railscasts.com/episodes/263-client-side-validations
EDIT
If you want to capture the 3 stages, you can save in your db. New, Validate, Finished and just use callbacks to save each stage and set the default to new. (You will have the change the data type of the validated attribute to string)
after_validation update attribute to "validate"
after_save update attribute to "Finished"
Then you can use an if elsif else conditions to check for the value of that attribute and render the tick and cross. Obviously, this isn't pretty and you should just use valid? and the errors? helpers.