Charlie Massry

Raw SQL on Rails

October 15, 2014

ActiveRecord is fantastic, but sometimes if you are doing complex joins and such, you may need to move away from ActiveRecord and write raw SQL. Consider an application where you need to join three tables together with one SQL query. How would you write this in SQL?

SELECT locations.name AS location_name,
  locations.street_address,
  locations.city,
  locations.state,
  users.title,
  users.first_name,
  users.last_name,
  rooms.id,
  rooms.name AS room_name,
  rooms.room_number,
  rooms.location_id
  FROM locations
  INNER JOIN users
  ON users.location_id = locations.id
  INNER JOIN rooms
  ON rooms.location_id = locations.id
  WHERE locations.name ILIKE '%Empire%'
  AND users.last_name ILIKE '%Empire%';

This is a mouthful but what is going on is pretty straightforward; still a few questions come to mind: how do you turn it into an active record object, how to you make the search dynamic, and how do you prevent SQL injection.

The answer to the first question is to wrap the SQL statement string so ActiveRecord will know how to understand it.

ActiveRecord::Base.
  connection.
  execute(
  <<-SQL
    SELECT locations.name AS location_name,
    locations.street_address,
    locations.city,
    locations.state,
    users.title,
    users.first_name,
    users.last_name,
    rooms.id,
    rooms.name AS room_name,
    rooms.room_number,
    rooms.location_id
    FROM locations
    INNER JOIN users
    ON users.location_id = locations.id
    INNER JOIN rooms
    ON rooms.location_id = locations.id
    WHERE locations.name ILIKE '%Empire%'
    AND users.last_name ILIKE '%Empire%';
  SQL
  )

This will execute it and return an ActiveRecord Object, but it will not be dynamic. You can just make it dynamic with ?, but it will be vulnerable to SQL injection, which is not something you want to do, so you will have to escape it.

def sql_query
  ActiveRecord::Base.
    connection.
    execute(
      sanitized_sql_statement
    )
end

def sanitized_sql_statement
  ActiveRecord::Base.send(
    :sanitize_sql_array,
    [
      sql_string,
      "%#{query}%",
      "%#{query}%"
    ]
  )
end

def sql_statement
  <<-SQL
    SELECT locations.name AS location_name,
      locations.street_address,
      locations.city,
      locations.state,
      users.title,
      users.first_name,
      users.last_name,
      rooms.id,
      rooms.name AS room_name,
      rooms.room_number,
      rooms.location_id
      FROM locations
      INNER JOIN users
      ON users.location_id = locations.id
      INNER JOIN rooms
      ON rooms.location_id = locations.id
      WHERE locations.name ILIKE ?
      AND users.last_name ILIKE ?;
  SQL
end

Of course you can rewrite this in ActiveRecord, but it might not seem as intuitive as the SQL statement.

Location.joins(:users, :rooms).select(
  "locations.name AS location_name",
  "locations.street_address",
  "locations.city",
  "locations.state",
  "users.title",
  "users.first_name",
  "users.last_name",
  "rooms.id",
  "rooms.name AS room_name",
  "rooms.room_number",
  "rooms.location_id"
).where(
  "locations.name ILIKE ? users.last_name ILIKE ?",
  "%#{query}%",
  "%#{query}%
)

Here you can see that locations is the main table that you are joining with the other tables and you are only returning certain columns. If you did select("*") you would get everything, including the users encrypted password; you are then performing the search query. One of the main problems I had with this was to know exactly when each particular method was supposed to be called. When you call SELECT in SQL it has to be the first statement but in ActiveRecord you are chaining the methods together (and doing what looks like a violation of the Law of Demeter), and the ordering of the methods matter so ActiveRecord can properly construct the SQL query.