rails - get the SQL of a model save - ruby-on-rails

I need to be able to output the SQL UPDATES that would be generated by Rails, without actually running them or Saving the records. I will be outputting the SQL updates to a file instead.
Is there a way to do this in Rails, without using string interpolation?
Is it possible to do something like the following:
p = Post.where (something)
p.some_value = some_new_value
p.to_sql??? # how to generate the update statement
rather than:
"UPDATE TABLE SET field_1 = #{new_field} WHERE ID = " etc etc

Took from #R.F. Nelson and wrap it to a method. You could just calling to_update_sql with your model as the argument to get the SQL.
def to_update_sql(model)
return '' if model.changes.empty?
table = Arel::Table.new(model.class.table_name)
update_manager = Arel::UpdateManager.new(model.class)
update_manager.set(model.changes.map{|name, (_, val)| [table[name], val] })
.where(table[:id].eq(model.id))
.table(table)
return update_manager.to_sql
end
post = Post.first
post.some_value = xxxx
to_update_sql(post)
# => UPDATE `posts` SET `some_value` = xxx WHERE `posts`.`id` = 1

Taken from this post:
You can achieve this goal with AREL:
# Arel::InsertManager
table = Arel::Table.new(:users)
insert_manager = Arel::InsertManager.new
insert_manager.into(table)
insert_manager.insert([ [table[:first_name], 'Eddie'] ])
sql_transaction = insert_manager.to_sql
File.open('file_name.txt', 'w') do |file|
file.write(sql)
end
# Arel::UpdateManager
table = Arel::Table.new(:users)
update_manager = Arel::UpdateManager.new
update_manager.set([[table[:first_name], "Vedder"]]).where(table[:id].eq(1)).table(table)
sql_transaction = update_manager.to_sql
File.open('file_name.txt', 'w') do |file|
file.write(sql)
end
Here you can find all Arel managers, like delete_manager.rb, select_manager.rb and the others.
Good read: http://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html

Related

Chaining multiple ActiveRecord `or` queries

I've got an array of columns that I want to loop through and optionally chain an or query onto an ActiveRecord query chain. I can get it to work, but the resulting query appends the or onto the query chain, therefore making the columns in my inital query optional. Here's my class:
class Claim
class MatchingAttributeFinder
ATTRIBUTE_GROUPS_TO_MATCH = [
["teacher_reference_number"],
["email_address"],
["national_insurance_number"],
["bank_account_number", "bank_sort_code", "building_society_roll_number"],
].freeze
def initialize(source_claim, claims_to_compare = Claim.submitted)
#source_claim = source_claim
#claims_to_compare = claims_to_compare
end
def matching_claims
claims = #claims_to_compare.where.not(id: #source_claim.id)
ATTRIBUTE_GROUPS_TO_MATCH.each do |attributes|
vals = values_for_attributes(attributes)
next if vals.blank?
concatenated_columns = "CONCAT(#{attributes.join(",")})"
claims = claims.or(
Claim.where("LOWER(#{concatenated_columns}) = LOWER(?)", vals.join)
)
end
claims
end
private
def values_for_attributes(attributes)
attributes.map { |attribute|
#source_claim.read_attribute(attribute)
}.reject(&:blank?)
end
end
end
The generated SQL looks like this:
SELECT "claims".* FROM "claims" WHERE (((("claims"."submitted_at" IS NOT NULL AND "claims"."id" != 'a7b25b99-4477-42b1-96ab-8262582c5541' OR (LOWER(CONCAT(teacher_reference_number)) = LOWER('0902344'))) OR (LOWER(CONCAT(email_address)) = LOWER('genghis.khan#mongol-empire.com'))) OR (LOWER(CONCAT(national_insurance_number)) = LOWER('QQ891011C'))) OR (LOWER(CONCAT(bank_account_number,bank_sort_code,building_society_roll_number)) = LOWER('34682151972654123456789/ABCD')))
But what I actually want is more like this:
SELECT "claims".* FROM "claims" WHERE "claims"."submitted_at" IS NOT NULL AND "claims"."id" != 'd6a53b4d-c569-49e6-a2ea-ac44b69b0451' AND (LOWER(concat(teacher_reference_number)) = LOWER('0902344') OR LOWER(concat(email_address)) = LOWER('genghis.khan#mongol-empire.com') OR LOWER(concat(national_insurance_number)) = LOWER('QQ891011C') OR LOWER(concat(bank_account_number,bank_sort_code,building_society_roll_number)) = LOWER('34682151972654123456789/ABCD'))
Is there any way to set up something like an empty scope that I can chain my OR queries to?
Try chaning all the "or" together first and then chain the original query
def matching_claims
claims = #claims_to_compare.where.not(id: #source_claim.id)
ors = nil
ATTRIBUTE_GROUPS_TO_MATCH.each do |attributes|
vals = values_for_attributes(attributes)
next if vals.blank?
concatenated_columns = "CONCAT(#{attributes.join(",")})"
aux = Claim.where("LOWER(#{concatenated_columns}) = LOWER(?)", vals.join)
if ors.nil?
ors = aux
else
ors = ors.or(aux)
end
end
claims.merge(ors)
end

How to get Rails 4 activerecord insert sql?

I'm trying to get the sql statement of activerecord dynamically.
e.g
pen = Pen.new
pen.description = "HB"
pen_sql = pen.insert_sql
puts pen_sql
=> "Insert into pens (description) values ('HB')"
You create you own method to get the insert statement something like this.
def get_insert_sql(from_dbmodel)
insert_sql = from_dbmodel.class.arel_table.create_insert.tap do |im|
im.insert(from_dbmodel.send(:arel_attributes_with_values_for_create, from_dbmodel.attribute_names))
end.to_sql
return insert_sql
end
pen = Pen.new
pen.description = "HB"
pen_sql = get_insert_sql(pen)
puts pen_sql
=> "Insert into pens (description) values ('HB')"
Tested in rails 4

Trouble adding "and" and "or" clauses in query through rails query interface

I am trying to run following query through Rails query interface but unable to translate my logic. The query is
Select f.* from feeds f
Left join feed_items fi on fi.id = f.feedable_id
where
f.feedable_type in ('Homework', 'Datesheet')
and
(
(fi.assignable_type = 'Level' and assignable_id IN (1)) or
(fi.assignable_type = 'Student' and assignable_id IN (1)) or
(fi.assignable_type = 'Section' and assignable_id IN (1))
)
Scenario:
I receive following params hash in my action containing filters which will be added dynamically in my query
{"page"=>"1", "limit"=>"2", "type_filter"=>["Homework", "Datesheet"], "assignable_filter"=>{"Student"=>"[2]", "Section"=>"[1]", "Level"=>"[1]"}}
So far, what I have done is joining the tables and added where clause for type filter but not sure how to dynamically add assignable_filters. Here is my rails code, options are params in following code
def get_feeds(options)
base = Feed.includes(:feed_item)
base = add_type_filters base, options
base = add_assignable_filters base, options
format_response base, options
end
def add_type_filters(base, options)
type_filter = options[:type_filter]
if !type_filter.nil? and type_filter.length > 0
base = base.where('feedable_type IN (?)', options[:type_filter])
end
base
end
def add_assignable_filters(base, options)
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
# code for adding filters combined with or conditions
end
# wrap the or conditions and join them with an and in main where clause
end
base
end
P.S I am using rails 5
There was no straight forward way of building the query dynamically. I had to construct the where string to solve the problem. My current solution is
def get_feeds(options)
params_hash = {}
type_filters = add_type_filters options, params_hash
assignable_filters = add_assignable_filters options, params_hash
where = type_filters
where = where ? "#{where} and (#{assignable_filters})" : assignable_filters
base = Feed.eager_load(:feed_item).where(where, params_hash)
format_response base, options
end
def add_type_filters(options, params_hash)
type_filter = options[:type_filter]
type_filter_sql = nil
if !type_filter.nil? and type_filter.length > 0
type_filter_sql = 'feeds.feedable_type in (:type_filter)'
params_hash[:type_filter] = type_filter
end
type_filter_sql
end
def add_assignable_filters(options, params_hash)
assignable_filter_sql = []
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
assignable_filter_sql.push("(feed_items.assignable_type = '#{key}' and feed_items.assignable_id IN (:#{key}))")
params_hash[key.to_sym] = JSON.parse(value)
end
end
assignable_filter_sql.join(' or ')
end

How to build a query with arbitrary placeholder conditions in ActiveRecord?

Assume I have an arbitrary number of Group records and I wanna query User record which has_many :groups, the catch is that users are queries by two bound fields from the groups table.
At the SQL level, I should end up with something like this:
SELECT * FROM users where (categories.id = 1 OR users.status = 0) OR(categories.id = 2 OR users.status = 1) ... -- to infinity
This is an example of what I came up with:
# Doesn't look like a good solution. Just for illustration.
or_query = groups.map do |g|
"(categories.id = #{g.category.id} AND users.status = #{g.user_status.id} )"
end.join('OR')
User.joins(:categories).where(or_query) # Works
What I think I should be doing is something along the lines of this:
# Better?
or_query = groups.map do |g|
"(categories.id = ? AND users.status = ? )".bind(g.category.id, g.user_status.id) #Fake method BTW
end.join('OR')
User.joins(:categories).where(or_query) # Works
How can I achieve this?
There has to be a better way, right?
I'm using Rails 4.2. So the shiny #or operator isn't supported for me.
I would collect the condition parameters separately into an array and pass that array (splatted, i.e. as an arguments list) to the where condition:
or_query_params = []
or_query = groups.map do |g|
or_query_params += [g.category_id, g.user_status.id]
"(categories.id = ? AND users.status = ?)"
end.join(' OR ')
User.joins(:categories).where(or_query, *or_query_params)
Alternatively, you might use ActiveRecord sanitization:
or_query = groups.map do |g|
"(categories.id = #{ActiveRecord::Base.sanitize(g.category_id)} AND users.status = #{ActiveRecord::Base.sanitize(g.user_status.id)})"
end.join(' OR ')
User.joins(:categories).where(or_query)

Display Json in view

Im having problems trying to display in my view information comming from a json file. I have already parse it.
Here is my error:
When assigning attributes, you must pass a hash as an argument.
Extracted source (around line #23):
21 # #new_member.constructors = [driver['Constructors'][0]['name']]
22 # #new_member.points = [driver['points']]
23 #new_member.from_json(json)
#members << #new_member
end
# #new_member.constructors = [driver['Constructors'][0]['name']]
# #new_member.points = [driver['points']]
#new_member.from_json(json)
#members << #new_member
end
This is my controller:
require 'open-uri'
require 'json'
url = "http://ergast.com/api/f1/2014/driverStandings.json"
data = JSON.parse(open(url).read)
standings = data['MRData']['StandingsTable']['StandingsLists'][0]['DriverStandings']
#members = Array.new
standings.each do |driver|
json = standings.to_json
#new_member = Member.new
# #new_member.position = [driver['position']]
# #new_member.givenName = [driver['Driver']['givenName']]
# #new_member.familyName = [driver['Driver']['familyName']]
# #new_member.constructors = [driver['Constructors'][0]['name']]
# #new_member.points = [driver['points']]
#new_member.from_json(json)
#members << #new_member
If I uncommented the lines in the controller and delete these lines
#new_member.from_json(json)
json = standings.to_json
I get the following in the view
Name: ["Lewis"]
Name: ["Nico"]
That view is really close but is not what i need, I need the data without [" "],
so the view that I need is something like:
1 Lewis Hamilton Mercedes 384
Thanks in advance.
Alternative 1
Change the line:
#new_member.givenName = [driver['Driver']['givenName']]
into:
#new_member.givenName = driver['Driver']['givenName']
removing the external [ and ], so that the field #new_member.givenName that you're populating becomes a string rather than an array containing one string.
Alternative 2
Is it possible that, when looping through the standings hash, the driver temp variable is a Hash?
In this case it may be sufficient to do:
#members = Array.new
standings.each do |driver|
#members << Member.new(driver)
end
Bonus: you probably don't need to assign an instance variable #new_member, that gets overwritten at each iteration of the loop

Resources