I am trying to begin work on building some API functionality in a Rails app and I’m just playing around with the concepts in a simple app where a Movie has_many :reviews and Review belongs_to :movie and I’d like a url that looks like /api/movies?review=mark. Here is more data for context:
2.6.3 :005 > m
=> #<Movie id: 12, title: "Wonder Woman", rating: "PG-13", total_gross: 0.821847012e9, created_at: "2019-08-19 13:48:08", updated_at: "2019-08-19 13:48:08", description: "When a pilot crashes and tells of conflict in the ...", released_on: "2017-06-02", director: "Patty Jenkins", duration: "141 min", image_file_name: "wonder-woman.png">
2.6.3 :006 > m.reviews
Review Load (0.2ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."movie_id" = ? LIMIT ? [["movie_id", 12], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Review id: 1, name: "mark", stars: 3, comment: "blah", movie_id: 12, created_at: "2019-08-19 13:49:44", updated_at: "2019-08-19 13:49:44">]>
I have a basic api build so I can do /api/movies and will return json with all movies, but not sure where to go from here for query functionality in the a get request. Some one point me in the right direction?
You say you want:
all reviews for movies that have the name "mark"
In which case, it seems like you should be calling:
/api/reviews?review%5Bname%5D%5B%5D=mark
Which will give you request something like:
Started GET "/api/reviews?review%5Bname%5D%5B%5D=mark" for ::1 at 2019-08-21 17:40:28 -0700
Processing by Api::ReviewsController#index as HTML
Parameters: {review: {name: ['mark']}}
Then, in your Api::ReviewsController, you could do something like:
class Api::ReviewsController < ApplicationController
def index
if params[:review]
#reviews = Review.where(review_params)
else
#reviews = Review.all
end
... do some stuff like rendering json
end
private
def review_params
params.require(:review).permit(name: [])
end
end
And that will return all reviews for movies where Review has a name value of mark. You could pass in multiple names and get back records for multiple reviewers.
You might want to check queryko gem for building API queries
Related
I am using Noticed gem for my notifications and I am trying to query for the notifications related to the post.
The notification stores the post as an object inside params.
#<Notification id: 10, params: {:post=>#<Post id: 3}>
I can target like
notification.params[:post][:id]
=> 3
What I am trying to accomplish is something like this:
Notifications.where(params[:post][:id] => 3)
Is there a way to do that or my approach should be different?
Edited:
Here is an example of a notification in the database:
#<Notification id: 10, recipient_type: "User", recipient_id: 8, type: "MessageNotification", params: {:user=>#<User id: 3, email: "test2#test2.com", created_at: "2021-01-06 23:34:46", updated_at: "2021-04-15 17:47:54", admin: true>, :conversation=>#<Mailboxer::Conversation id: 6, subject: "Hello", created_at: "2021-05-14 00:14:41", updated_at: "2021-05-14 00:26:06">}, read_at: nil, created_at: "2021-05-14 00:26:06", updated_at: "2021-05-14 17:11:50">
squema.rb
t.jsonb "params"
A query suggested by #Joel_Blum and the slq call:
>> current_user.notifications.where('params #> ?', {conversation: {id: 6}}.to_json).to_sql
=> "SELECT \"notifications\".* FROM \"notifications\" WHERE \"notifications\".\"recipient_id\" = 8 AND \"notifications\".\"recipient_type\" = 'User' AND (params #> '{\"conversation\":{\"id\":6}}')"
Returns => #<ActiveRecord::AssociationRelation []>
It seems the gem creates either a jsonb or json column depending on your db.
So what want is perform a search on a json(b) column
For postgres for example it can be done like this
Notification.where('params #> ?', {post: {id: 3}}.to_json)
For whatever reason I am getting an empty array on a notification that I am sure exists.
query: Notification.where('params #> ?', {comment: {id: testc.id}}.to_ json)
SQL: "SELECT \"notifications\".* FROM \"notifications\" WHERE (params #> '{\"comment\":{\"id\":656}}')"
Noticed has a built in way of querying for objects you send in the params.
If you add has_noticed_notifications to the Post model, you should then be able to call #post.notifications_as_post
This is in the Noticed readme here. I definitely came here and found this question before I found the details in the readme!
I am trying to convert a hash to an activerecord relation but am not able to do so. I intend to use the activerecord relation to sort and then filter the Category table. The end goal is to create an instance method to filter the top 5 visited categories, which i can then use/call in the controller. This is what i have in mind:
Category model:
def top_5_visited
Article.joins(:categories).group('categories.name').sum(:impressions_count)
// Add sort
// Limit to top 5
end
Category controller:
#categories = Category.top_5 visited
A hash {"Simula"=>7, "Haskell"=>5, "JavaScript"=>10, "C#"=>112} will be created through the following query:
total_count = Article.joins(:categories).group('categories.name').sum(:impressions_count)
I have also tried to convert it to an array using sort_by method:
total_count_sorted = total_count.sort_by {|_key, value| value}
I have googled "convert array to activerecord relation" and referenced this post, but testing this:
Category.where(id: total_count_sort.map(&:id))
in the rails console, brings up this error:
NoMethodError: undefined method id for ["Simula", 7]:Array
What you want to do start from the inverse end (Category) and use an aggregate in the ORDER clause.
Category.joins(:articles)
.order('SUM(articles.impressions_count) DESC')
.group(:id)
.limit(5)
irb(main):005:0> Category.joins(:articles).order("SUM(articles.impressions_count) DESC").group('categories.id').limit(5)
Category Load (1.5ms) SELECT "categories".* FROM "categories" INNER JOIN "articles" ON "articles"."category_id" = "categories"."id" GROUP BY categories.id ORDER BY SUM(articles.impressions_count) DESC LIMIT $1 [["LIMIT", 5]]
=> #<ActiveRecord::Relation [#<Category id: 4, name: "C#", created_at: "2017-11-15 15:06:32", updated_at: "2017-11-15 15:06:32">, #<Category id: 3, name: "JavaScript", created_at: "2017-11-15 15:06:32", updated_at: "2017-11-15 15:06:32">, #<Category id: 1, name: "Simula", created_at: "2017-11-15 15:03:37", updated_at: "2017-11-15 15:03:37">, #<Category id: 2, name: "Haskell", created_at: "2017-11-15 15:06:32", updated_at: "2017-11-15 15:06:32">]>
And you should create a class method - not an instance method as this is basically just a scope and does not make sense to call on an instance.
class Category < ApplicationRecord
has_many :articles
def self.order_by_article_impressions
self.joins(:articles)
.order('SUM(articles.impressions_count)')
.group(:id)
end
def self.top_5_visited
order_by_article_impressions.limit(5)
end
# Or use `scope` which is just syntactic sugar
scope(:top_5_visited) -> { order_by_article_impressions.limit(5) }
end
Change the code to:
Category.where(id: total_count_sort.map(&:last))
I'm playing with rails console:
a = Event.where("location_id IS NULL")
Event Load (0.7ms) SELECT "events".* FROM "events" WHERE (start_date >= '2015-02-20' OR end_date >= '2015-02-20') AND (location_id IS NULL)
=> #<ActiveRecord::Relation [#<Event id: 58, name: "BENNY GREB CLINIC w Hard Rock Cafe Warszawa", start_date: "2015-02-21", end_date: nil, location: "Hard Rock Cafe Warsaw,ul. Złota 59,Warsaw,Poland", logo_url: "https://fbcdn-sphotos-c-a.akamaihd.net/hphotos-ak-...", website: "", facebook: "https://www.facebook.com/events/535924839876600", created_at: "2015-02-19 17:38:29", updated_at: "2015-02-19 17:38:29", logo_file_file_name: nil, logo_file_content_type: nil, logo_file_file_size: nil, logo_file_updated_at: nil, location_id: nil>]>
So location: "Hard Rock Cafe Warsaw,ul. Złota 59,Warsaw,Poland"
But when I do:
2.2.0 :002 > a.first.location
I get
=> nil
Why?
You have a location attribute and a location association and rails is using the association. You need to rethink your database structure so they are named differently.
As per the first comment in the question:
You might be using an active record association:
has_one :location
or
belongs_to :location
Active record will give precedence to the relation over the model attribute.
Try adding:
has_one/belongs_to :location, as: 'related_location'
To the model in order to ensure location is always defining the attribute. And you will access to the related location (using the location_id) using the aliased method.
I'm seeing some weird behaviour in my models, and was hoping someone could shed some light on the issue.
# user model
class User < ActiveRecord::Base
has_many :events
has_and_belongs_to_many :attended_events
def attend(event)
self.attended_events << event
end
end
# helper method in /spec-dir
def attend_events(host, guest)
host.events.each do |event|
guest.attend(event)
end
end
This, for some reason inserts the event with id 2 before the event with id 1, like so:
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 2, name: "dummy-event", user_id: 1>, #<Event id: 1, name: "dummy-event", user_id: 1>
But, when I do something seemlingly random - like for instance change the attend_event method like so:
def attend_event(event)
self.attended_events << event
p self.attended_events # random puts statement
end
It gets inserted in the correct order.
#<ActiveRecord::Associations::CollectionProxy [#<Event id: 1, name: "dummy-event", user_id: 1>, #<Event id: 2, name: "dummy-event", user_id: 1>
What am I not getting here?
Unless you specify an order on the association, associations are unordered when they are retrieved from the database (the generated sql won't have an order clause so the database is free to return things in whatever order it wants)
You can specify an order by doing (rails 4.x upwards)
has_and_belongs_to_many :attended_events, scope: -> {order("something")}
or, on earlier versions
has_and_belongs_to_many :attended_events, :order => "something"
When you've just inserted the object you may see a different object - here you are probably seeing the loaded version of the association, which is just an array (wrapped by the proxy)
I have four tables:
argument with fields
id
comments with
id
comment_id
argument_id
user_id
users
id
nicknames with
id
proposal_id
user_id
name
each argument has many comments,
each comment belongs to a user,
each user has a specific nickname in the argument.
When I fetch the argument comments from DB, I would like to include also the nicknames of each author.
The answer is about the ActiveRecord query I don't know how to write.
I tried with
#argument.comments.includes(:user => :nicknames)
but it doesn't seems to work and when I get the nickname through
nickname = #argument.nicknames.find_by_user_id(comment.user.id)
it executes the query...
[1m[36mNickname Load (0.6ms)[0m [1mSELECT "nicknames".* FROM "nicknames" WHERE "nicknames"."argument_id" = 59 AND "nicknames"."user_id" = 9 LIMIT 1[0m
any suggestion?
You can tell if an association is loaded with loaded?.
What is happening here, if I understand your problem, is that you are trying to run a finder on an ActiveRecord::Relation. Quickly browsing through the code, it does not appear that it will try to see if a collection is loaded before it issues the query. It does, however, take a block that will avoid multiple queries. For example (the model names have been changed because I am using a sample project I created for another question):
c = Canteen.first
Canteen Load (0.2ms) SELECT "canteens".* FROM "canteens" LIMIT 1
=> #<Canteen id: 1, name: "Really good place", created_at: "2012-12-13 00:04:11", updated_at: "2012-12-13 00:04:11">
c.meals.loaded?
=> false
c.meals.find {|m| m.id == 3}
Meal Load (0.2ms) SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" = 1
=> #<Meal id: 3, canteen_id: 1, name: "Banana Pie", price: #<BigDecimal:7fcb6784fa78,'0.499E1',18(45)>, created_at: "2012-12-13 00:37:41", updated_at: "2012-12-13 00:37:41">
You see in the last example that ActiveRecord issues the query to load the associated records. This is because ActiveRecord is calling to_a on the association, forcing the entire set to be loaded, and then filtering based on the block conditions. Obviously, this is not ideal.
Let's try again, eager loading the association.
c = Canteen.includes(:meals).first
Canteen Load (0.2ms) SELECT "canteens".* FROM "canteens" LIMIT 1
Meal Load (0.2ms) SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" IN (1)
=> #<Canteen id: 1, name: "Really good place", created_at: "2012-12-13 00:04:11", updated_at: "2012-12-13 00:04:11">
c.meals.loaded?
=> true
c.meals.find {|m| m.id == 3}
=> #<Meal id: 3, canteen_id: 1, name: "Banana Pie", price: #<BigDecimal:7fcb68b596f0,'0.499E1',18(45)>, created_at: "2012-12-13 00:37:41", updated_at: "2012-12-13 00:37:41">
In the last example here, you see that the collection is not loaded again. Instead, the block is used to filter the already loaded records.
As you can see below, even if the records are loaded, ActiveRecord will issue a query to grab the associated record:
c.meals.loaded?
=> true
c.meals.find(1)
Meal Load (0.1ms) SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" = 1 AND "meals"."id" = ? LIMIT 1 [["id", 1]]
=> #<Meal id: 1, canteen_id: 1, name: "Enchiladas", price: #<BigDecimal:7fcb6584ce88,'0.699E1',18(45)>, created_at: "2012-12-13 00:04:40", updated_at: "2012-12-13 00:04:40">
SELECT "meals".* FROM "meals" WHERE "meals"."canteen_id" = 1 AND "meals"."id" = 3
=> [#<Meal id: 3, canteen_id: 1, name: "Banana Pie", price: #<BigDecimal:7fcb68b808e0,'0.499E1',18(45)>, created_at: "2012-12-13 00:37:41", updated_at: "2012-12-13 00:37:41">]
Maybe something like :
#argument.includes(:comments => [{ :user => :nicknames }])
Didn't try it though...
You can try something like this to include more than one table
User.find(:all, :include => Room.find(:all,:include => :review))