Paginating a shuffled ActiveRecord query - mysql

I am attempting to paginate a shuffled ActiveRecord query. The syntax for doing this using the Kaminari gem is:
#users = Kaminari.paginate_array(User.all.shuffle).page(params[:page]).per(20)
The issue with this is that User.all is re-shuffled on each pagination request, causing duplicate records to be called. Is there any way to prevent this kind of duplication?

You need to pass seed for rand between queries
params[:seed] ||= Random.new_seed
srand params[:seed].to_i
#users = Kaminari.paginate_array(User.all.shuffle).page(params[:page]).per(20)
And in view add params[:seed] to all kaminari links to pages

As KandadaBoggu points out above, retrieving all of the User records from the database is inefficient when you only need 20. I would suggest using MySQL's RAND() function to perform the randomization before you return from the database. You can still pass a seed value to RAND() to make sure the shuffling only happens once per session.
For example:
class User < ActiveRecord::Base
def self.randomized(seed = nil)
seed = seed.to_i rescue 0
order("RAND(#{seed})")
end
end
class UsersController < ApplicationController
before_filter :set_random_seed
def index
#users = User.randomized(session[:seed]).page(params[:page]).per(20)
end
private
def set_random_seed
session[:seed] ||= Random.new_seed
end
end
I don't have a MySQL installation to test against, but this should perform better than your original code.

You can also do this:
class UsersController < ApplicationController
USERS_SEED = 1000 # Or any another not-so-big number
def set_random_seed
session[:seed] ||= Random.rand(USERS_SEED)
end
end
Because Random.new_seed will generate most likely the same result if your data isn't that big.

Related

Removing password_digest from API json?

I'm using Sinatra to make a simple little API. I have not been able to figure out a way to remove the 'password_digest' field from the JSON I'm outputting. Well, I know of a long way that I can do it, but I have a feeling there is a much simpler way.
get "/users/all" do
content_type :json
#users = User.all
response = #users.map do |user|
user = user.to_h
user.delete("password_digest")
user
end
response.to_json
end
All I'm trying to do is remove the password_digest field from the output. Is there a simple way to do this? I've tried searching with no luck.
get "/users/all" do
content_type :json
#users = User.all
#users.to_json(except: [:password_digest])
end
You can also overide #as_json on the model to remove the attribute completely from serialization:
class User < ActiveRecord::Base
def as_json(**options)
# this coerces the option into an array and merges the passed
# values with defaults
excluding = [options[:exclude]].flatten
.compact
.union([:password_digest])
super(options.merge(exclude: excluding))
end
end
You should be able to do this:
get "/users/all" do
content_type :json
#users = User.all
response = #users.map do |user|
user = user.to_h # If your data is already a hash, you don't need this line.
user.delete(:password_digest) # <-- If your keys are symbolized
user.delete("password_digest") # <-- If your keys are strings
user
end
response.to_json
end

convert mysql query into rails query

Hi all I have a problem converting mysql query into rails query.
I have these models -
class User < ApplicationRecord
has_many :comments, foreign_key: "commenter_id"
end
class Comment < ApplicationRecord
belongs_to :commenter, class_name: "User"
end
Can anyone help me out with converting following query into rails query-
UPDATE comments
INNER JOIN users on comments.commenter_id = users.id
SET comments.deleted_at = users.deleted_at
WHERE users.deleted_at IS NOT NULL
I am trying to make soft-delete comments whose commenter was softly deleted.
UPDATE 1:
so far I can able to do it by using this-
User.only_deleted.includes(:comments).find_each do |u|
u.comments.update_all(deleted_at: u.deleted_at)
end
But I want to do this on single query without having to iterate over the result.
UPDATE 2:
I am using acts_as_paranoid gem, so unscoping user is needed and my final query became:
User.unscoped{Comment.joins(:commenter).where.not(users: {deleted_at: nil}).update_all("comments.deleted_at = users.deleted_at")
This should work on MySQL:
Comment
.joins(:user)
.where.not(users: { deleted_at: nil })
.update_all("comments.deleted_at = users.deleted_at")
This won't work on Postgres since its missing a FROM clause for users.
A less performant but polyglot option is:
Comment
.joins(:user)
.where.not(users: { deleted_at: nil })
.update_all("deleted_at = ( SELECT users.deleted_at FROM users WHERE comments.id = users.id )")
This is still probably an order of magnitude better than iterating through the records in Ruby since you eliminate the traffic delay between your app server and the db.
From your comments, I think this is what you want:
Comment.where.not(user_id: nil).each { |comment| comment.update_attributes(deleted_at: comment.user.deleted_at)
Or slightly more readable:
Comment.all.each do |comment|
next unless comment.user.present?
comment.update_attributes(deleted_at: comment.user.deleted_at)
end
The code below should execute number of queries corresponding to deleted_users and without loading User and any associated Comments in memory
deleted_users_data_arr = User.only_deleted.pluck(:id, :deleted_at)
deleted_users_data_arr.each do |arr|
deleted_user_id = arr[0]
user_deleted_at = arr[1]
Comment.where(commenter_id: deleted_user_id).update_all(deleted_at: user_deleted_at)
end

Rails 4: ActiveRecord or MySQL query where no related models have attribute

Having a tough time with this one. I have a Job model, and a JobStatus model. A job has many statuses, each with different names (slugs in this case). I need an 'active' method I can call to find all jobs where none of the associated statuses has a slug of 'dropped-off'.
class Job < ActiveRecord::Base
belongs_to :agent
has_many :statuses, :class_name => "JobStatus"
validates :agent_id,
:pickup_lat,
:pickup_lng,
:dropoff_lat,
:dropoff_lng,
:description,
presence: true
class << self
def by_agent agent_id
where(agent_id: agent_id)
end
def active
#
# this should select all items where no related job status
# has the slug 'dropped-off'
#
end
end
end
Job Status:
class JobStatus < ActiveRecord::Base
belongs_to :job
validates :job_id,
:slug,
presence: true
end
The closest I've gotten so far is:
def active
joins(:statuses).where.not('job_statuses.slug = ?', 'dropped-off')
end
But it's still selecting the Job that has a dropped-off status because there are previous statuses that are not 'dropped-off'. If i knew the raw sql, I could probably work it into activerecord speak but I can't quite wrap my head around it.
Also not married to using activerecord, if the solution is raw SQL that's fine too.
Job.where.not(id: JobStatus.where(slug: 'dropped-off').select(:job_id))
will generate a nested subquery for you.
Not the cleanest method, but you could use two queries.
# Getting the ID of all the Jobs which have 'dropped-off' JobStatuses
dropped_off_ids = JobStatus.where(slug: 'dropped-off').pluck(:job_id)
# Using the previous array to filter the Jobs
Job.where.not(id: dropped_off_ids)
Try this:
def active
Job.joins(:statuses).where.not('job_statuses.slug' => 'dropped-off')
end
or this:
def active
Job.joins(:statuses).where('job_statuses.slug != ?', 'dropped-off')
end
I think you may want to reevaluate your data model somewhat. If the problem is that you're turning up old statuses when asking about Job, you likely need to have column identifying the current status for any job, i.e. job.statuses.where(current_status: true)
Then you can very easily grab only the rows which represent the current status for all jobs and are not "dropped-off".
Alternatively, if I'm misunderstanding your use case and you're just looking for any job that has ever had that status, you can just go backwards and search for the status slugs first, i.e.
JobStatus.where.not(slug: "dropped-off").map(&:job)

Rails ActiveRecord Database with just one row

Is there a way for a database to only allow one row (e.g. for site-wide settings) ?
class Whatever < ActiveRecord::Base
validate :there_can_only_be_one
private
def there_can_only_be_one
errors.add_to_base('There can only be one') if Whatever.count > 0
end
end
In Rails 4:
class Anything < ActiveRecord::Base
before_create :only_one_row
private
def only_one_row
false if Anything.count > 0
end
end
Silent errors are bad, then
class Anything < ActiveRecord::Base
before_create :only_one_row
private
def only_one_row
raise "You can create only one row of this table" if Anything.count > 0
end
end
Is there just one column in this row? If not, adding new columns with a migration may be overkill. You could at least make this table contain 'name' and 'value' columns and validate by uniqueness on name.

Ruby libxml parsing and inserting to database

I am currently trying to read from an xml file which records the jobs on a PBS. I have succesfullly managed to parse the code, but am unable to insert the objtects into my database, i receive this error:
"You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.delete"
This is my Model:
require 'xml/libxml'
class Job < ActiveRecord::Base
JOB_DIR = File.join('data', 'jobs')
attr_reader :jobid, :user, :group, :jobname, :queue, :ctime
attr_reader :qtime, :etime, :start, :owner
def initialize(jobid, user, group, jobname, queue, ctime, qtime, etime, start, owner)
#jobid, #user, #group, #jobname, #queue = jobid, user, group, jobname, queue
#ctime, #qtime, #etime, #start, #owner = ctime, qtime, etime, start, owner
end
def self.find_all()
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
node.find('jobid').to_a.first.content,
node.find('user').to_a.first.content,
node.find('group').to_a.first.content,
node.find('jobname').to_a.first.content,
node.find('queue').to_a.first.content,
node.find('ctime').to_a.first.content,
node.find('qtime').to_a.first.content,
node.find('etime').to_a.first.content,
node.find('start').to_a.first.content,
node.find('owner').to_a.first.content
)
end
jobs
end
end
An my Model Controller:
class JobController < ApplicationController
def index
#jobs = Job.find_all()
end
def create
#jobs = Job.find_all()
for job in #jobs
job.save
end
end
end
I would appreciate any help...Thank you!
I'm not sure on the causes of the error message you're seeing because I can't see anywhere that you're trying to invoke a delete method, however this does seem like a slightly confused use of ActiveRecord.
If you have a jobs database table with fields jobid, user, group, jobname etc. then ActiveRecord will create accessor methods for these and you should not be using attr_reader or overriding initialize. You should also not be setting values value instance variables (#jobid etc.) If you don't have such fields on your table then there is nothing in your current code to map the values from the XML the database fields.
Your def self.find_all method should probably be along the lines of:
def self.build_from_xml
jobs = []
input_file = "#{JOB_DIR}/1.xml"
doc = XML::Document.file(input_file)
doc.find('//execution_record').each do |node|
jobs << Job.new(
:jobid => node.find('jobid').to_a.first.content,
:user => node.find('user').to_a.first.content,
...
Rails used to have a method of its own find_all to retrieve all existing records from the database so your method name is probably a bit misleading. Rails tends to use the build verb to mean create a new model object but don't save it yet so that's why I've gone with a name like build_from_xml.