I am attempting to access the results of a join where columns of both tables are specified as part of a projection.
I have 2 models (Rails 4.2.11; Arel 6.0.4; Ruby 2.5.3)
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime not null
# updated_at :datetime not null
class User < ActiveRecord::Base
has_many :photos
end
# Table name: photos
#
# id :integer not null, primary key
# name :string(255)
# created_by_id :integer
# assigned_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# user_id :integer
class Photo < ActiveRecord::Base
belongs_to :user
end
creators = User.arel_table.alias('creators')
updaters = User.arel_table.alias('updaters')
photos = Photo.arel_table
photos_with_credits = photos.
join(photos.join(creators, Arel::Nodes::OuterJoin).on(photos[:created_by_id].eq(creators[:id]))).
join(photos.join(updaters, Arel::Nodes::OuterJoin).on(photos[:assigned_id].eq(updaters[:id]))).
project(photos[:name], photos[:created_at], creators[:name].as('creator'), updaters[:name].as('editor'))
# generated SQL
SELECT photos.name, photos.created_at, creators.name AS creator, updaters.name AS editor
FROM photos
INNER JOIN (SELECT FROM photos LEFT OUTER JOIN users creators ON photos.created_by_id = creators.id)
INNER JOIN (SELECT FROM photos LEFT OUTER JOIN users updaters ON photos.updated_by_id = updaters.id)
How I'd like to process the result
photos_with_credits.map{|x| "#{photo.name} - copyright #{photo.created_at.year} #{photo.creator}, edited by #{photo.editor}"}.join('; ')
This may be a very dumb question, but ...
I have not been able to find a way to use the SelectManager result to produce a meaningful output since map was deprecated (& removed) from the SelectManager class.
I would appreciate your help.
Simplified the activerecord / arel query builder
amended code
photos_with_credits = Photo.select([photos[:name], photos[:created_at], creators[:name].as('creator'), updaters[:name].as('editor')]).
joins(photos.outer_join(creators).on(photos[:created_by_id].eq(creators[:id])).join_sources).
joins(photos.outer_join(updaters).on(photos[:assigned_id].eq(updaters[:id])).join_sources)
photos_with_credits.map do |photo|
puts "#{photo.name} - copyright #{photo.created_at.year} #{photo.creator}, edited by #{photo.editor}".light_blue
end
amended SQL (simpler)
SELECT photos.name, photos.created_at, creators.name AS creator, updaters.name AS editor
FROM photos
LEFT OUTER JOIN users creators ON photos.created_by_id = creators.id
LEFT OUTER JOIN users updaters ON photos.assigned_id = updaters.id
We should figure out a way to trigger SelectManage and get ActiveRelation result, then we can iterate it by using map, etc.
So I would like to suggest:
Photo.join(photos_with_credits.join_sources).map do |photo|
"#{photo.name} - copyright #{photo.created_at.year} #{photo.creator}, edited by #{photo.editor}"}.join('; ')
end
Related
I am re-writing a program called Emissions Gateway to the new version of Ruby on Rails.
I have a method that was written with syntax from a gem called Squeel and I am having a very hard time re-writing it. I have been failing at it for over 4 hours and can't seem to get it figured out.
This is the method right here, it is in a model called datalogger.rb along with the schema information for the datalogger.rb model.
# == Schema Information
#
# Table name: dataloggers
#
# id :integer not null, primary key
# project_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# original_file_name :string(255)
# original_content_type :string(255)
# original_file_size :integer
# original_updated_at :datetime
# status :string(255) default("incomplete")
# hours :integer
# readings_total :integer
# readings_start :datetime
# readings_stop :datetime
# direct_upload_url :string(255)
# version :string(255)
# comments :string(255)
# bypass :boolean default(FALSE)
# device_id :integer
# device_name :string(255)
# device_description :string(255)
# device_serial :integer
# md5 :string(255)
# user_id :integer
# reported_at :datetime
# user_reported_id :integer
# reported :boolean
def stats(temperatures)
unless bypass
#temperatures = temperatures
#stats = {}
#cumulative_percent_at = 0
#cumulative_readings = 0
#temperatures.each_cons(2) do |current_n, next_n|
# puts "Evaluating #{current_n} and #{next_n}"
#stats["#{next_n}"] = {}
# CHANGED v0.009 9/27/2021 Scott Milella
# readings_at = readings.where{(temperature.gt current_n) & (temperature.lteq next_n)}.sum(:frequency)
readings_at = Reading.where(:temperature > current_n).and(:temperature <= next_n).sum(:frequency)
#stats["#{next_n}"][:readings] = readings_at
# puts "Readings at: #{readings_at}"
# #cumulative_readings = #stats.map{|_, v| v[:readings] }.sum
# puts "Cumulative Readings: #{cumulative_readings}"
percent_at = ((readings_at.to_f / readings_total) * 100 )
#stats["#{next_n}"][:time_at] = percent_at
#cumulative_percent_at += percent_at
# puts "Percent at: #{percent_at}%"
# puts "Cumulative Percent at: #{#cumulative_percent_at}"
percent_over = 100 - #cumulative_percent_at
#stats["#{next_n}"][:over] = percent_over
# puts "Percent Over: #{percent_over}%"
# puts "Progress: #{#cumulative_readings}/#{readings_total} readings"
end
end
This is the method I changed:
readings_at = Reading.where(:temperature > current_n)
.and(:temperature <= next_n).sum(:frequency)
You can see what I changed above in the method as well as I indicate it with # CHANGED. It is giving me this error called comparison of Symbol with 100 failed which makes NO Sense to me because the :symbol is an integer from another model called Reading.
Here is that model:
# == Schema Information
#
# Table name: readings
#
# id :integer not null, primary key
# temperature :integer
# frequency :integer
# datalogger_id :integer
#
class Reading < ActiveRecord::Base
belongs_to :datalogger
attr_accessible :frequency, :temperature, :datalogger_id
validates_presence_of :frequency, :temperature, :datalogger_id
end
I don't understand why I can't compare an Integer with an Integer regardless if it is in a symbol or not? Do I have the Syntax wrong or something? It isn't giving me a syntax error. I have tried about 1000 other ways to write it and I get a variety of errors from > not found in Array to all kinds of other things. If anyone wants to see the whole datalogger.rb model I will post it, it's rather long and it seems to be just this method that the problem exists in.
Here is a single line I have captured from the SQL out of the current version of Emissions Gateway that is working: You can see the number 272 should be current_n and the 150 is the next_n I can verify those values on the better_errors console. So I don't understand where I am going wrong. I am guessing it might have something to do with the each_cons method perhaps which I do not understand.
I modified it so you could see the SQL all in one place, otherwise it was displaying as one long line. I will show it after just in case it is confusing:
2021-09-27T18:50:49.173173+00:00 app[web.1]: (1.5ms) SELECT SUM("readings"."frequency")
AS sum_id FROM "readings"
WHERE "readings"."datalogger_id" = 272
AND (("readings"."temperature" > 100
AND "readings"."temperature" <= 150))
The SQL as it comes out
2021-09-27T18:50:49.173173+00:00 app[web.1]: (1.5ms) SELECT SUM("readings"."frequency") AS sum_id FROM "readings" WHERE "readings"."datalogger_id" = 272 AND (("readings"."temperature" > 100 AND "readings"."temperature" <= 150))
If anyone can point out how I need to re-write this method I would be greatly appreciative, I have tried for hours and am getting noplace.
Here is the instructions for squeel in case anyone needs to see the instructions.
https://github.com/activerecord-hackery/squeel
I wish that gem had NEVER been written, has caused me so much pain it is unreal!
Thank You,
Scott
Ok, let's take a deep look into your query:
readings_at = Reading.where(:temperature > current_n).and(:temperature <= next_n).sum(:frequency)
Both :temperature > current_n and :temperature <= next_m are comparing symbols (left side) with integers (right side). That's why you are getting an ArgumentError.
The Rails syntax to achieve what you are doing is:
readings_at = Reading.where('temperature > ? AND temperature <= ?', current_n, next_n).sum(:frequency)
Or, if you prefer, adding multiples where will add an AND clause to your query. So the below is equivalent:
readings_at = Reading.where('temperature > ?', current_n).where('temperature <= ?', next_n).sum(:frequency)
Using the ? guarantee that Rails is going to "clean" this input for you in order to prevent SQL injection.
There are two models:
# == Schema Information
#
# Table name: news
#
# id :integer not null, primary key
# title :string not null
# content :text not null
# scope :string not null
# created_at :datetime not null
# updated_at :datetime not null
# person_id :integer not null
# == Schema Information
#
# Table name: likes
#
# id :integer not null, primary key
# like :boolean
# person_id :integer not null
# news_id :integer not null
Relation
news has many likes
like belongs to news
I want to get most liked news from query. Query should substract count of likes equal true from likes equal false. The highest number is most liked news.
What I tried:
#count_true_likes = Like.where('likes.like = ?', true).group(:news_id).count
#count_false_likes = Like.where('likes.like = ?', false).group(:news_id).count
Result is Hash with id and counted likes. I don't have idea how to subtract in query positive likes from negative likes, and do it for every news.
This kind of querying becomes prohibitively slow very quickly, as your dataset grows. A common workaround is to cache number of upvotes and downvotes. For example
# Table name: news
#
# id :integer not null, primary key
# title :string not null
# content :text not null
# scope :string not null
# created_at :datetime not null
# updated_at :datetime not null
# person_id :integer not null
#
# upvotes_count :integer not null
# downvotes_count :integer not null
# vote_result :integer not null
Where vote_result is a cached upvotes_count - downvotes_count.
Then simply do
News.order(vote_result: :desc).limit(10) # top 10 articles
The downside is, of course, that you need to maintain those cached counters (increase/decrease corresponding ones when you register a vote).
I resolved my problem:
#most_liked_news_id = Like.group(:news_id)
.select('news_id, SUM(case when likes.like then 1 else -1 end) as max_positive')
.order('max_positive desc').map(&:news_id).first
#most_liked_news = News.find(#most_liked_news_id)
In console:
Course.ids.count
=> 1766
Course.pluck(:id).count
=> 1766
Course.ids.uniq.count
=> 1529
Course.count
=> 1529
It's normal?
small comment - model Course uses ancestry (gem).
UPD1:
Generated sql:
Learn::Course.ids.count
(5.4ms) SELECT "learn_courses"."id" FROM "learn_courses" LEFT OUTER JOIN "learn_course_translations" ON "learn_course_translations"."learn_course_id" = "learn_courses"."id"
=> 1766
Learn::Course.count
(1.5ms) SELECT COUNT(*) FROM "learn_courses"
=> 1529
hmm...
UPD2:
Schema Information
#
# Table name: learn_courses
#
# id :integer not null, primary key
# name :string(255)
# position :integer
# created_at :datetime
# updated_at :datetime
# ancestry :string(255)
# course_type :string(255)
# article :string(255)
# item_style :integer
# hidden :boolean
# score :integer default(0)
# next_id :integer
# first :boolean
You should be able to work around this with
Learn::Course.pluck('distinct learn_courses.id')
The problem is that LEFT OUTER JOIN with learn_course_translations, which must have multiple rows per Learn::Course, resulting in the same learn_courses.id appearing several times. pluck doesn't care about distinctness, so it just passes them all back.
Maybe ancestry adds default_scope to your model. Try to check it with
Learn::Course.unscoped.ids.count
I am trying to do a basic belongs_to/has_many association but running into a problem. It seems that the declaring class's foreign key column is not being updated. Here are my models:
#
# Table name: clients
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime
# updated_at :datetime
#
class Client < ActiveRecord::Base
has_many :units
...
attr_accessible :name
end
#
# Table name: units
#
# id :integer not null, primary key
# client_id :integer
# name :string(255)
# created_at :datetime
# updated_at :datetime
#
class Unit < ActiveRecord::Base
belongs_to :client
...
attr_accessible :name
end
When I open rails console I do the following:
#This works as it should
c1 = Client.create(:name => 'Urban Coding')
u1 = c1.units.create(:name => 'Birmingham Branch')
The above gives me the correct results. I have a client and a unit. The unit has the client_id foreign key field populated correctly.
#This does not work.
c1 = Client.create(:name => 'Urban Coding')
u1 = Unit.create(:name => 'Birmingham Branch')
u1.client = c1
I feel the above should have the same effect. However this is not the case. I have a unit and a client but the units client_id column is not populated. Not sure exactly what I am doing wrong here. Help is appreciated. Let me know if you need more information.
You're simply not saving u1, hence no change to the database.
If you want it assigned and saved in a single operation, use update_attribute
u1.update_attribute(:client, c1)
Yep, I think if you save it the ID will get set.
The first syntax is much better. If you don't want to do the save action right away, which create does for you, then use build:
c1 = Client.create(:name => 'Urban Coding')
u1 = c1.units.build(:name => 'Birmingham Branch')
# do stuff with u1
u1.save
This works:
c1 = Client.create(:name => 'Urban Coding')
u1 = Unit.create(:name => 'Birmingham Branch')
u1.client_id = c1.id
u1.save
c1.save
but the other way is the better way to create it.
I have a twitter-like feed in my web app. I've tried to optimize the query as much as possible but it still freezes when it's loading the "tweets" from PostgresSQL database.
Twitter, github, facebook, feeds are so smooth and fast.
What's the best way to accomplish that?
Regards.
**Edit: the code is the one in railstutorial.org.
http://railstutorial.org/chapters/following-users#sec:scopes_subselects_and_a_lambda
# Return microposts from the users being followed by the given user.
scope :from_users_followed_by, lambda { |user| followed_by(user) }
private
# Return an SQL condition for users followed by the given user.
# We include the user's own id as well.
def self.followed_by(user)
followed_ids = user.following.map(&:id).join(", ")
where("user_id IN (#{followed_ids}) OR user_id = :user_id",
{ :user_id => user })
end
# Table name: microposts
#
# id :integer not null, primary key
# content :string(255)
# user_id :integer
# created_at :datetime
# updated_at :datetime
#
Sorry for not providing enough information.**
Did you put any index on your tables?