Rails: Eager loading as_json includes - ruby-on-rails

render :json => {
"playlist" => playlist_description,
"songs" => #playlist.songs.as_json(:include => {:playlist_songs => {:only => [:id, :position]}})
}
The above code results in 1+N queries to the database, one to load playlist_songs for each song.
The playlist is preloaded in #playlist.
This is so slow, how can I optimize?

My guess: You're not eager-loading the playlist_songs at the moment. You're currently waiting until the as_json call - after which all the songs have been loaded - and then the code has to iterate over every song and fetch the playlist_songs then.
My guess (this is totally untested and may include bugs)
#playlist.songs.all(:include => :playlist_songs).as_json(:include => {:playlist_songs => {:only => [:id, :position]}})
AFAICT, this should first eager load all the songs and the playlist_songs... and then render as json.

I would highly recommend integrating with a JSON builder such as rabl. It will make your life 10x easier moving forward, and is extremely nice to separate the "view" of the JSON representation. I made the switch a couple months ago and haven't looked back.
Within your controller:
#playlist = Playlist.where(:id => params[:id]).includes(:playlist_songs)
Then the rabl template could be something like this:
object #playlist
attribute :description
child :playlist_songs do
attributes :id, :position
end

render :json => {
"playlist" => playlist_description,
"songs" => #playlist.songs.all.as_json(:include => {:playlist_songs => {:only => [:id, :position]}})
}
^ guess

Related

Rails - How To Check if Defined before adding into new Object?

I'm working with the Amazon Product Advertising API and I'm trimming its responses within my controller to render customized JSON, but its responses change a lot depending on the product category, so I need to write code that can catch all of the ways it changes. I only need a few pieces of data, so how can I simply check to see if those pieces exist within Amazon's API response before including them in my own custom JSON?
Note: I'm using this gem to integrate with Amazon's API. It returns the API responses in its own objects.
#results = (Amazon's API response)
#custom_response = []
#results.items.each do |product|
#new_response = OpenStruct.new(
:id => product.asin,
:source => "amazon",
:title => product.title,
:price => #product_price,
:img_small => #images[0],
:img_big => #images[1],
:category => product.product_group,
:link => "http://amazon.com/" )
#custom_response << #new_response
end
You can try something like this:-
#new_response = OpenStruct.new(:source => "amazon", :link => ".....")
#new_response.id = product.asin if product.asin.present?
#new_response.title = product.title if product.title.present?
other attributes....

Capturing Webpages Using Nokogiri -- Need to semi-persistent data

I've got a module that does webscraping. I use this method a number of times, since it captures all the data on the webpage.
def page_as_xml(uri)
#page_as_xml ||= Nokogiri::HTML(open(uri))
end
Since I'll use the above method a handful of times for each page, it makes sense to keep it in an instance variable. However, how do I "empty out" the instance variable after I'm done?
All the webcsraping ends up in a hash (see below). If I don't "empty out" the instance variable, then the same page_as_xml data will get used for each page.
:page1 =>
{
:url => #page1,
:title => download_title(#page1),
:meta_tags => download_robots_tags(#page1)
},
:page2 =>
{
:url => #page2,
:title => download_title(#page2),
:meta_tags => download_robots_tags(#page2)
},
:page3 =>
{
:url => #page3,
:title => download_title(#page3),
:meta_tags => download_robots_tags(#page3)
},
How about make it a hash:
#pages_as_xml[uri] ||= Nokogiri::HTML(open(uri))
Now you don't have to worry about emptying it (unless memory is an issue).
I don't really understand why you need to call it more than once though. Also why do you call it page_as_xml if it is html?

render model to json and use encode64 on one of the model's columns

I am trying to send a model as json. The model has binary data in one of it's columns. For another model I have used
format.json {self.encode64(#resource_type.data).to_json}
with success, but in that case I only wanted the data column, and not the title etc. What can I do when I want contents from several columns, where only one column's content should be encoded with encode64?
In the following code, I don't know where to put the self.encode64 method.
format.json { render :json => #resource.to_json(:only => [:id, :title, :data])}
How can I do this?
You have a few options here.
You could add a data_base64 method to your model that returned the data in base-64 format and then use the :methods option to to_json in your controller:
#resource.to_json(:only => [ :id, :title ], :methods => :data_base64)
That would give you a data_base64 key in the JSON instead of data but that might not be a problem.
You could also use as_json to get a hash and fix the encoding in the controller:
json = #resource.as_json(:only => [ :id, :title, :data ])
json['resource']['data'] = self.encode64(json['resource']['data'])
render :json => json
You can use as_json in model to override this behaviour like this
def as_json(options={})
{ :name_of_resource => { :created_at => created_at, binary => encode64(self.data) } }
end
You need to specify how he should serialize whole model into json.
Cheers!

How to use jquery-Tokeninput and Acts-as-taggable-on

This is how you use autocomplete with jQuery Tokeninput and ActsAsTaggableOn.
In my situation i am using a nested form but it shouldnt matter. Everything below is code that works.
Code
Product Model:
attr_accessible :tag_list # i am using the regular :tag_list
acts_as_taggable_on :tags # Tagging products
Products Controller:
#1. Define the tags path
#2. Searches ActsAsTaggable::Tag Model look for :name in the created table.
#3. it finds the tags.json path and whats on my form.
#4. it is detecting the attribute which is :name for your tags.
def tags
#tags = ActsAsTaggableOn::Tag.where("tags.name LIKE ?", "%#{params[:q]}%")
respond_to do |format|
format.json { render :json => #tags.map{|t| {:id => t.name, :name => t.name }}}
end
end
Routes:
# It has to find the tags.json or in my case /products/tags.json
get "products/tags" => "products#tags", :as => :tags
Application.js:
$(function() {
$("#product_tags").tokenInput("/products/tags.json", {
prePopulate: $("#product_tags").data("pre"),
preventDuplicates: true,
noResultsText: "No results, needs to be created.",
animateDropdown: false
});
});
Form:
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tags.map(&:attributes).to_json %>
Issue 1(SOLVED)
Must have the line:
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name }}}
Note - You can use #tags.map here as well and you dont have to change the form either.
Below are the 2 issues on why you needed to do this:
I have the following Tag: {"id":1,"name":"Food"}. When I save a Product, tagged "Food", it should save as ID: 1 when it searches and finds the name "Food". Currently, it saves a new Tag with a new ID that references the "Food" ID, i.e. {"id":19,"name":"1"}. Instead, it should be finding the ID, showing the name, and doing a find_or_create_by so it doesn't create a new Tag.
Issue 2(SOLVED)
When I go to products/show to see the tags by doing <%= #product.tag_list %>. The name appears as "Tags: 1", when it really should be "Tags: Food".
How can I fix these issues?
You should define a route in your routes.rb which should handle products/tags path. You can define it like:
get "products/tags" => "products#tags", :as => :tags
Thus should give you a tags_path helper which should evaluate to /products/tags. This should get rid of the errors you mentioned in the question. Be sure to add this route before defining resources :product in your routes.rb
Now onto acts-as-taggable-on, I haven't used this gem, but you should look at method all_tag_counts documentation. Your ProductsController#tags method will need some changes on the following lines. I am not sure if its exactly what would be required, as I use Mongoid and can't test it out.
def tags
#tags = Product.all_tag_counts.(:conditions => ["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", "%#{params[:q]}%"])
respond_to do |format|
format.json { render :json => #tags.collect{|t| {:id => t.name, :name => t.name } }
end
end
little add-on:
If you want to create the tags on the fly, you could do this in your controller:
def tags
query = params[:q]
if query[-1,1] == " "
query = query.gsub(" ", "")
Tag.find_or_create_by_name(query)
end
#Do the search in memory for better performance
#tags = ActsAsTaggableOn::Tag.all
#tags = #tags.select { |v| v.name =~ /#{query}/i }
respond_to do |format|
format.json{ render :json => #tags.map(&:attributes) }
end
end
This will create the tag, whenever the space bar is hit.
You could then add this search setting in the jquery script:
noResultsText: 'No result, hit space to create a new tag',
It's a little dirty but it works for me.
There is a bug in Application.js code. There is an extra ) after "/products/tags.json". Remove the extra ). The code should be:
$("#product_tags").tokenInput("/products/tags.json", {
prePopulate: $("#product_tags").data("pre"),
preventDuplicates: true,
noResultsText: "No results, needs to be created.",
animateDropdown: false
});
I don't know if this is the entirety of your error, but you are not hitting the proper URL with the tokenInput plugin.
This
$("#product_tag_list").tokenInput("/products/tags.json"), {
should be
$("#product_tag_list").tokenInput("/products.json"), {
As I said, I don't know if this is the only problem you are having, but if you change this, does it work?
EDIT:
I have never used ActsAsTaggableOn. Does it create a Tag model for you to use?
From the looks of it on github, if you wanted to query all tags, you might have to use its namespace as opposed to just Tag, meaning ActsAsTaggableOn::Tag. For example, you can see how they access Tags directly in some of the specs.
I had problems with editing the tags if for example the model failed to validate,
I changed
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tags.map(&:attributes).to_json %>
to
<%= p.text_field :tag_list,
:id => "product_tags",
"data-pre" => #product.tag_list.map {|tag| {:id => tag, :name => tag } }.to_json %>
If the form failed to validate on first submission, it was creating tags as the ID's of the tags it had created on subsequent submissions.
Two notes: if you're getting the tags changed by numbers on the POST request, use:
tokenValue: "name"
And if you're trying to add non-existent tags, use (undocumented):
allowFreeTagging: true

Rails - Get 3 ID's in a form

I have a group#view page, that is accessed by a Person. In this page, the Person can see the members of the group via methods I developed. The problem is that I need to create the model Honors using the Id from the group, the id from the person accessing the page, and the id from a member of this group.
In my Honors controller I have:
def create
#person = Person.find(current_person)
#honor = Honor.create(:group => Group.find(params[:group_id]),
:person => Person.find(current_person), :honored => Person.find(current_person))
if #honor.save
...
end
The problem is in this :honored => Person.find(current_person), that is not getting the right ID and I don`t know how to get it.
In my view:
<% #asked_groupmembership.each do |agm| %>
<% form_for(:honor, :url => honors_path(:group_id => #group.id, :person => current_person.id,:honor => agm.member.id)) do |f| %>
Any help?
Thanks.
If you need 3 components to properly create an honor record, you need to pass them from the form. You seem to be doing that part correctly.
:group_id => #group.id
:person => current_person.id
:honor => agm.member.id
To create the record, access the passed variables.
Honor.create(:group => Group.find(params[:group_id]),
:person => Person.find(params[:person]),
:honored => Person.find(params[:honor]))
Understanding the above isn't the most efficient, but used for demonstrative purposes. You'd likely want to avoid redundant database hits, e.g. :person => current_person rather than another query

Resources