Is there a way to convert a Rails model into an insert query?
For instance, if I have a model like:
m = Model.new
m.url = "url"
m.header = "header"
How can I get the corresponding SQL query ActiveRecord would generate if I did m.save?
I want to get: "INSERT INTO models(url, header) VALUES('url', 'header')" if possible.
Note: I don't want to actually save the model and get the query back (from log file, etc). I want to get the query IF I chose to save it.
On Rails 4.1, I found the below code snippet working:
record = Post.new(:title => 'Yay', :body => 'This is some insert SQL')
record.class.arel_table.create_insert
.tap { |im| im.insert(record.send(
:arel_attributes_with_values_for_create,
record.attribute_names)) }
.to_sql
Thanks to https://coderwall.com/p/obrxhq/how-to-generate-activerecord-insert-sql
Tested in Rails 3.2.13: I think I got it right this time, it definitely does not persist to the db this time. It also won't fire validations or callbacks so anything they change won't be in the results unless you've called them some other way.
Save this in lib as insert_sqlable.rb and you can then
#in your models or you can send it to ActiveRecord::Base
include InsertSqlable
Then it is model.insert_sql to see it.
#lib/insert_sqlable
module InsertSqlable
def insert_sql
values = arel_attributes_values
primary_key_value = nil
if self.class.primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == self.class.primary_key
}]
if !primary_key_value && connection.prefetch_primary_key?(self.class.table_name)
primary_key_value = connection.next_sequence_value(self.class.sequence_name)
values[self.class.arel_table[self.class.primary_key]] = primary_key_value
end
end
im = self.class.arel_table.create_insert
im.into self.class.arel_table
conn = self.class.connection
substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
binds = substitutes.map do |arel_attr, value|
[self.class.columns_hash[arel_attr.name], value]
end
substitutes.each_with_index do |tuple, i|
tuple[1] = conn.substitute_at(binds[i][0], i)
end
if values.empty? # empty insert
im.values = Arel.sql(self.class.connectionconnection.empty_insert_statement_value)
else
im.insert substitutes
end
conn.to_sql(im,binds)
end
end
It turns out the code is in ActiveRecord::Relation and not ActiveRecord::Persistence. The only significant change is the last line which generates the sql instead of performing it.
If you dont want to save the model you call m.destroy when you are done with the object.
You can log the sql query by debugging it like this
Rails.logger.debug "INSERT INTO models(url, header) VALUES(#{m.url}, #{m.header}).inspect
After search a lot over the Internet and forums, I think I found a better solution for your problem: just requires two line of code.
I found a good gem that do exactly what you want, but this gem only works for Rails 3.2 and older. I talked with author and he doesn't want support this gem anymore. So I discovered by myself how to support Rails 4.0 and now I'm maintaining this gem.
Download the "models-to-sql-rails" gem here, supporting Rails 4.0 and older.
With this gem, you can easily do the following. (the examples inside values are just a joke, you will get the correct values when using it in your object).
For objects:
object.to_sql_insert
# INSERT INTO modelName (field1, field2) VALUES ('Wow, amaze gem', 'much doge')
For array of objets:
array_of_objects.to_sql_insert
# INSERT INTO modelName (field1, field2) VALUES ('Awesome doge', "im fucking cop")
# INSERT INTO modelName (field1, field2) VALUES ('much profit', 'much doge')
# (...)
Just see the Github of this project and you'll find how to install and use this wonderful gem.
Related
I am fetching data from CSV files and saving them to the database. Here's the simplified code structure (it's a rake task):
... loading CSV tools...
car = Car.new
car.tender_load_id = data.element[8]
car.brand = data.element[2]
car.color = 'green'
...
car.build_car_metadata(mbol: help_var[:mbol], ...)
car.first_registration = data.element[21]
car.skip_registration_code_validation = true # for not creating registration_code
if existing_car = Car.where("cars.tender_load_id ILIKE ?", "%#{car.tender_load_id}%").first
# car already exists => update
existing_car.update(car)
puts "Car exists, updating car with ID #{existing_car.id}."
else
car.save!
end
When I run this take task, I get the following error message:
NoMethodError: undefined method `reject' for #<Car:0x007fa3e2063a78>
and it points out to this line:
existing_car.update(car)
How do I make this work? I unfortunately cannot place the if-else part on the beginning of the rake task, the structure needs to be like this.
Thank you in advance.
You need to update the existing car with just the attributes, you can not pass in a full model.
Build the attributes, then create or update depending on whether a car exists already:
attributes = {}
attributes[:color] = ...
attributes[:brand] = ...
attributes[:tender_load_id] = ...
if (car = Car.where("cars.tender_load_id ILIKE ?", "%#{attributes[:tender_load_id]}%").first)
car.update(attributes)
else
Car.create!(attributes)
end
Note 1: It seems there can be multiple cars with the same tender_load_id. This means that the query for existing car will return an arbitrary record. Perhaps you want to add an order to that query.
Note 2: The way you query seems brittle. Isn't there a better ID in the CSV?
Note 3: attributes ending in _id are usually foreign keys. So perhaps find a better name for this attribute.
I'm just wondering if there's a way to access the raw SQL that's executed for an update_all ActiveRecord request. As an example, take the simple example below:
Something.update_all( ["to_update = ?"], ["id = ?" my_id] )
In the rails console I can see the raw SQL statement so I'm guessing it's available for me to access in some way?
PS - I'm specifically interested in update_all and can't change it to anything else.
Thanks!
If you look at the way update_all is implemented you can't call to_sql on it like you can on relations since it executes directly and returns an integer (the number of rows executed).
There is no way to tap into the flow or get the desired result except by duplicating the entire method and changing the last line:
module ActiveRecord
# = Active Record \Relation
class Relation
def update_all_to_sql(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
if eager_loading?
relation = apply_join_dependency
return relation.update_all(updates)
end
stmt = Arel::UpdateManager.new
stmt.set Arel.sql(#klass.sanitize_sql_for_assignment(updates))
stmt.table(table)
if has_join_values? || offset_value
#klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
else
stmt.key = arel_attribute(primary_key)
stmt.take(arel.limit)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
end
#- #klass.connection.update stmt, "#{#klass} Update All"
stmt.to_sql
end
end
end
The reason you see the log statements is that they are logged by the connection when it executes the statements. While you can override the logging its not really possible to do it for calls from a single AR method.
If you have set RAILS_LOG_LEVEL=debug Rails shows you which SQL statement it executed.
# Start Rails console in debug mode
$ RAILS_LOG_LEVEL=debug rails c
# Run your query
[1] pry(main)> Something.update_all( ["to_update = ?"], ["id = ?" my_id] )
SQL (619.8ms) UPDATE "somethings" WHERE id = 123 SET to_update = my_id;
# ^it prints out the query it executed
I have a model Product that has attribute description and code which is an index.
I would like to alter the product in code based on a CSV file.
What is faster?
#p = Product.find_by_code(row[:code])
if #p.description != row[:desc]
#p.update_attribute(:description, row[:desc])
or
#p = Product.find_by_code(row[:code])
#p.update_attribute(:description, row[:desc])
Let's consider all cases, such as descriptions are equal and not equal at all.
How is = comparison implemented for strings and texts?
You should use the ruby Benchmark module and directly measure that !
require 'benchmark'
Benchmark.bm do |x|
x.report('sort!') do
#p = Product.find_by(code: row[:code])
if #p.description != row[:desc]
#p.description = row[:desc]
p.save
end
end
x.report('sort') do
#p = Product.find_by(code: row[:code])
#p.description = row[:desc]
p.save
end
end
Ruby on Rails is clever enough to know whether an attribute has actually changed, and so won't roundtrip to the database to update a field when it hasn't changed. You can see this on the Rails console (rails c) if you run your update_attribute code with the same value, and then with a changed value - you'll only see the SQL log output when it's changed.
If you use update_attributes instead (which takes a hash of attributes to change) and there is nothing to update, you'll see it does begin and end a transaction with the database, albeit with no commands within it.
Hope that helps!
How to build in rails postgis point, then geohash and save them into database before send response to client? I would like to make it through ST_MakePoint and ST_GeoHash function, I prefer to avoid execute SQL and extracting data by [0]["st_makepoint"], if it is possible how to insert this functions to execute them automatically when inserting all attributes? I've installed squeel gem, maybe can I merge this functions to the query?
My current rails code:
before_save :set_geopoint
def set_geopoint
#attributes -> {"latitude" => 51.90,"longitude" => 16.42,"geopoint" => nil}
#self.geopoint = "ST_MakePoint(#{latitude}, #{longitude})")" #not working
#self.geopoint = ActiveRecord::Base.connection.execute("SELECT ST_MakePoint(#{latitude}, #{longitude})")[0]["st_makepoint"]
#self.geohash = "ST_GeoHash(#{self.geopoint})"
#self.geohash = ActiveRecord::Base.connection.execute("SELECT ST_GeoHash(ST_SetSRID(#{self.geopoint},4326),5);").first["st_geohash"]
end
I did it through SQL before trigger function but I'm still looking for rails approach.
CREATE FUNCTION geopoint_trigger() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO posts( geopoint ) VALUES( ST_GeomFromText('POINT(' || NEW.latitude || ' ' || NEW.longitude || ')') );
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
To generate a point that you can save in a postgis enabled database, you need to generate that point with a factory. What you're doing here:
self.geopoint = "ST_MakePoint(#{latitude}, #{longitude})")" #not working
is just setting self.geopoint to a string and trying to save it. As your database geopoint field is (I assume) set to accept a point, it fails.
I suggest you use the rgeo gem to add geo factories to your models. Add it to your Gemfile.
https://github.com/rgeo/rgeo
You need to make sure that in your migration, you are using a point as column type like this:
t.point :geopoint, geographic: true
In your Post model, you then need to specify a factory like this:
RGEO_FACTORY = RGeo::Geographic.spherical_factory(srid: 4326)
And you also need to tell rgeo what factory to use on your geopoint column.
set_rgeo_factory_for_column :geopoint, RGEO_FACTORY
Now in your before_save, simply do:
self.geopoint = RGEO_FACTORY.point(latitude, longitude)
and it should work...
EDIT
If you want to use Postgis functions in your Rails models to get the GeoHash for instance, then you could do something like this:
post_geohash = Post.select("ST_GeoHash(geopoint) as geohash").where(id: some_post_id).geohash
You could also create an instance method that does that on your Post model:
def geohash
Post.select("ST_GeoHash(geopoint) as geohash").where(id: id).geohash
end
Not sure this works as it's not tested but you get the idea.
I am doing the following:
#group_coach = GroupCoach.find("groups_count < '9'" )
I have a groups_count column in my db that is being updated by a counter_cache => true method in the Group model.
I know this isn't right. Because the error it spits out: Couldn't find GroupCoach with ID=groups_count < '9'
I have reviewed the Rails Guides:
client = Client.find(10)
Client.where("orders_count = '2'")
The second option does run in the localhost but isn't actually returning a GroupCoach... It just returns groupcoach...
What is the proper syntax for this?
Have you tried:
#group_coach = GroupCoach.where("groups_count < 9")
To clarify: #group_coach will be a collection of records. So you can't call #group_coach.name because all that will do is give you "GroupCoach" (the name of the class). Instead, you would need to iterate over the items:
#group_coach.each do |group_coach|
puts group_coach.name
end