With many Rails projects, there is occasionally a need to bypass the normal ActiveRecord API, and get closer to the low-level SQL API. Here are a few useful methods that bypass the confines of everyday ActiveRecord.
#find_by_sql
users = User.find_by_sql("SELECT * FROM users WHERE id = 1")
This returns model instances for the given SQL string.
#find_by_sql
also accepts parametrized values, like this:
users = User.find_by_sql(["SELECT * FROM users WHERE id = ?", 1])
#select_all
users = ActiveRecord::Base.connection.select_all("
SELECT id, first_name, created_at
FROM users
WHERE id = 1")
users[0]
{
"id" => 202,
"first_name" => "Trystan",
"created_at" => "2015-09-05 15:44:51.597326"
}
This returns an array-like object of type ActiveRecord::Result
. Each item in
the collection is a Hash
representing a row in the results.
The ActiveRecord::Result
contains information about the table and its columns.
It knows how to convert column values to some of their corresponding Ruby
types.
#execute
users = ActiveRecord::Base.connection.execute("SELECT id, first_name, created_at FROM users WHERE id=1")
users[0]
{
"id" => "1",
"first_name" => "Trystan",
"created_at" => "2015-09-05 15:44:51.597326"
}
This returns an array-like object that is specific on the database driver. For
PostgreSQL, this will be a PG::Result
. Each item in the collection is a
Hash
. The values of the Hash are strings and nils. No conversion is performed
to convert the values to the appropriate Ruby type, other than NULL to nil.
#quote
The select_all
and execute
methods have no built-in mechanism for escaping
values for the SQL statement.
Something like this is bad practice:
users = ActiveRecord::Base.connection.select_all("
SELECT * FROM users WHERE email='#{email}'")
Because we’re not sure what is in email
. It could have an SQL injection
attack, or it could just have unexpected characters that will break
the query. To cover these
cases, ActiveRecord provides #quote
:
conn = ActiveRecord::Base.connection
users = conn.select_all("
SELECT * FROM users WHERE email=#{conn.quote(email)}")
Notice that the SQL statement does not have single quote marks around the email.
#quote
does that automatically.
#sanitize_sql_array
Using #quote
can get unwieldy as more and more values need to be escaped.
There is a private method called #sanitize_sql_array
that brings back the
parameterized escape mechanism that is present in ActiveRecord’s higher-level
API.
sql = ActiveRecord::Base.send(:sanitize_sql_array,
["SELECT * FROM users WHERE email=?", email])
users = ActiveRecord::Base.connection.select_all(sql)
#connection_config
Sometimes you need to introspect the database connection details. Maybe you have an external utility that performs bulk operations against the database, and it bypasses ActiveRecord entirely.
config = Rails.application.config.database_configuration
{
"common" => {
"adapter" => "postgres",
"encoding" => "utf8",
"postgis_extension" => true,
"username" => "user",
"password" => "password",
"host" => "127.0.0.1",
"port" => 5432
},
"development" => {
"adapter" => "postgres",
"encoding" => "utf8",
"postgis_extension" => true,
"username" => "user",
"password" => "password",
"host" => "127.0.0.1",
"port" => 5432,
"database" => "database_development"
},
"production" => {
"adapter" => "postgres",
"encoding" => "utf8",
"postgis_extension" => true,
"username" => "user",
"password" => "password",
"host" => "127.0.0.1",
"port" => 5432,
"database" => "database_production"
}
}
#with_connection
Using a database connection and not returning is like opening a file and not closing it. It can lead to connection leaks in some contexts. ActiveRecord provides a block-style pattern for using a database connection:
ActiveRecord::Base.connection_pool.with_connection do |conn|
users = conn.select_all("
SELECT * FROM users WHERE email=#{conn.quote(email)}")
end
#structure_load
For times when you need to load a bunch of SQL statements from a file,
ActiveRecord provides #structure_load
:
config = Rails.application.config.database_configuration['development']
sql_file = "tmp/foo.sql"
ActiveRecord::Tasks::DatabaseTasks.structure_load(config, sql_file)