I'm trying to find out how rails converts a hash such as (This is an example please do not take this literally I threw something together to get the concept by I know this query is the same as User.find(1)):
{
:select => "users.*",
:conditions => "users.id = 1",
:order => "username"
}
Into:
SELECT users.* FROM users where users.id = 1 ORDER BY username
The closest thing I can find is ActiveRecord::Base#find_every
def find_every(options)
begin
case from = options[:from]
when Symbol
instantiate_collection(get(from, options[:params]))
when String
path = "#{from}#{query_string(options[:params])}"
instantiate_collection(format.decode(connection.get(path, headers).body) || [])
else
prefix_options, query_options = split_options(options[:params])
path = collection_path(prefix_options, query_options)
instantiate_collection( (format.decode(connection.get(path, headers).body) || []), prefix_options )
end
rescue ActiveResource::ResourceNotFound
# Swallowing ResourceNotFound exceptions and return nil - as per
# ActiveRecord.
nil
end
end
I'm unsure as to how to modify this to just return what the raw mysql statement would be.
So after a few hours of digging I came up with an answer although its not great.
class ActiveRecord::Base
def self._get_finder_options options
_get_construct_finder_sql(options)
end
private
def self._get_construct_finder_sql(options)
return (construct_finder_sql(options).inspect)
end
end
adding this as an extension gives you a publicly accessible method _get_finder_options which returns the raw sql statement.
In my case this is for a complex query to be wrapped as so
SELECT COUNT(*) as count FROM (INSERT_QUERY) as count_table
So that I could still use this with the will_paginate gem. This has only been tested in my current project so if you are trying to replicate please keep that in mind.
Related
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
I have zero or many filter params being sent from a json request.
the params may contain:
params[:category_ids"]
params[:npo_ids"]
etc.
I am trying to retreive all Projects from my database with the selected ids. Here is what I have currently:
def index
if params[:category_ids].present? || params[:npo_ids].present?
#conditions = []
#ids = []
if params["category_ids"].present?
#conditions << '"category_id => ?"'
#ids << params["category_ids"].collect{|x| x.to_i}
end
if params["npo_ids"].present?
#conditions << '"npo_id => ?"'
#ids << params["npo_ids"].collect{|x| x.to_i}
end
#conditions = #ids.unshift(#conditions.join(" AND "))
#projects = Project.find(:all, :conditions => #conditions)
else ...
This really isn't working, but hopefully it gives you an idea of what I'm trying to do.
How do I filter down my activerecord query based on params that I'm unsure will be there.
Maybe I can do multiple queries and then join them... Or maybe I should put a filter_by_params method in the Model...?
What do you think is a good way to do this?
In rails 3 and above you build queries using ActiveRelation objects, no sql is executed until you try to access the results, i.e.
query = Project.where(is_active: true)
# no sql has been executed
query.each { |project| puts project.id }
# sql executed when the first item is accessed
The syntax you are using looks like rails 2 style; hopefully you are using 3 or above and if so you should be able to do something like
query = Project.order(:name)
query = query.where("category_id IN (?)", params[:category_ids]) if params[:category_ids].present?
query = query.where("npo_ids IN (?)", params[:npo_ids]) if params[:npo_ids].present?
#projects = query
I solved this. here's my code
def index
if params[:category_ids].present? || params[:npo_ids].present?
#conditions = {}
if params["category_ids"].present?
#conditions["categories"] = {:id => params["category_ids"].collect{|x| x.to_i}}
end
if params["npo_ids"].present?
#conditions["npo_id"] = params["npo_ids"].collect{|x| x.to_i}
end
#projects = Project.joins(:categories).where(#conditions)
else
basically it stored the .where conditions in #conditions, which looks something like this when there's both categories and npos:
{:categories => {:id => [1,2,3]}, :npo_id => [1,2,3]}
Then inserting this into
Project.joins(:categories).where(#conditions)
seems to work.
If you're filtering on a has_many relationship, you have to join. Then after joining, make sure to call the specific table you're referring to by doing something like this:
:categories => {:id => [1,2,3]}
This question already has answers here:
Rails, how to sanitize SQL in find_by_sql
(6 answers)
Closed 8 years ago.
Thanks for your help with this one! I'm working on a legacy Rails application and I've checked a few sources but haven't been able to make them work for my situation. In Rails 2.3, I've got a search form (index.html.erb) and a controller (search.rb). The controller uses the search fields to build a string, which is saved in the variable #search_string.
#search_string = ""
To show the results of the search, the controller uses:
#location_matches = Location.paginate_by_sql("select * from locations where #{#search_string} order by nickname asc", :page => params[:page], :per_page => 20)
What is the simplest way for me to sanitize the above? I tried
#location_matches = Location.paginate_by_sql('SELECT * FROM locations WHERE #{#search_string} = ?', #search_string)
This throws the error ArgumentError (parameter hash expected). FYI, the #search_string is built with form fields like this:
FORM
<label for="city">City</label>
<input id="city" name="city" size="30" type="text" value="" />
CONTROLLER
if params[:city] != ""
#search_string << "and city like '%#{params[:city]}%' "
if #first_term == 'y'
#search_string = #search_string.gsub('and ', " ")
#first_term = 'n'
end
end
I'm pretty sure the individual fields aren't properly parameterized, but rather than go through every field and try and fix it, I was looking for a faster solution by just tweaking the paginate_by_sql statement (I'm new to Rails and am just trying to make a quick fix since the whole app will eventually need upgrading.)
EDIT
I followed the steps in Rails, how to sanitize SQL in find_by_sql and added an initializer. I also changed my statement to the below. However, when I run my search, I get no results. I also had to delete the ascending condition because it threw an error. Any ideas are very welcome!
Initializer
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
Controller
query = Location.escape_sql(["SELECT * from locations WHERE #{#search_string} = ?", params[:#search_string]])
#location_matches = Location.paginate_by_sql(query, :page => params[:page], :per_page => 20)
Log (Looks the same as the code that returns results)
Parameters: {"city"=>"New York", "commit"=>"Search", "search"=>{"size_category"=>"", "state"=>""}}
The short answer is: you really can't do this quickly.
I'd recommend updating the search-string builder to, instead of creating just a single string, to build the array-style set of finders eg: ["field = ?", param]
eg:
search_string = ""
search_vals = []
if params[:city].present?
search_string << "and city like ? "
search_vals << "%#{params[:city]}%"
end
if params[:whatever].present?
search_string << "and whatever like ? "
search_vals << "%#{params[:whatever]}%"
end
Then when you want to do the final search, you could use it like this:
search_string = "SELECT * from locations WHERE #{search_string}"
# turns it into the array-with question-mark syntax
search_query = [search_string] + search_vals
# if paginate_by_sql takes array, you can use it straight up here:
#location_matches = Location.paginate_by_sql(search_query, :page => params[:page], :per_page => 20)
# otherwise use your "escape_sql" variant first
#location_matches = Location.paginate_by_sql(Location.escape_sql(search_query), :page => params[:page], :per_page => 20)
For example I have index action:
def index
if params[:query]
#pharmaceutics = Pharmaceutic.where("name LIKE ?", params[:query])
elsif params[:code]
#pharmaceutics = Pharmaceutic.where("barcode LIKE ?", params[:code])
else
#pharmaceutics = Pharmaceutic.all
end
end
And when I send two params: code and query I would like to filter my Pharmaceutics using both of them. I have MySQL database.
I would probably use scoped method, like this:
def index
scope = Pharmaceutic.scoped # Pharmaceutic.all if you use Rails 4
scope = scope.where('name LIKE ?', params[:query]) if params[:query].present?
scope = scope.where('barcode LIKE ?', params[:code]) if params[:code].present?
#pharmaceutics = scope
end
You can also write your custom scopes and replace where(...) with them to make the code clearer.
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.