The SQL and The AR (ActiveRecord)

Executing Complex Queries with ActiveRecord

Learning new ways to communicate with data and computers is always exiting, but not without pitfalls. When the path is not straightforward, typing simple functions feels particularly frustrating if you already have mastered an alternate way to accomplish the task, but are suddenly forced to change your approach. This happened to me while doing the Model Class Methods lab in the Full Stack Webdev Curriculum. I could find the information needed by typing raw SQL queries, but the preset tests required for the answers to be returned as ActiveRecord:Relation-type objects.

While tedious to compose, SQL syntax is rather easy to follow even when the query encompasses several join tables and grouping or aggregate functions. The logic of building an SQL query is always the same: SELECT what you want FROM where you want JOINING them with tables ON foreign keys and if you need to GROUP the results state what criteria they are supposed to be HAVING. For example:

SELECT * FROM table INNER JOIN other_table ON table.id = other_table.table_id INNER JOIN another_table ON other_table.id = another_table.other_table_id GROUP BY table.id HAVING COUNT(table.column) = N

Now, I was trying to chain AR queries together to get the same results and felt as if AR refused to listen to my orders.

AR query language works by injecting bits of SQL into the query string to be built. To be able to understand how to translate your SQL query into an AR format, you need to know the correct method name for each SQL - insertion method as well as be vary of distincting between AR calculations methods and query methods. The latter return Fixnum (.average, .sum, .minimum, .maximum, and .count methods), Hash(.count method when called after .group method), or Array(.ids and .pluck) objects, and thus have to be placed last in the query chain, even when it would linguistically make no sense.

The most common AR query methods are .where, .where.not, .limit, .order, .group, .having and .joins. These all return AR relation objects that you can use to chain further queries together. The .joins method executes inner joins for you; but remember that its functionality is wholly dependent on you defining the correct associations in your AR models.

The find_by_sql that allows you to write your whole query in raw SQL is an exception from the above queries:

"The results will be returned as an array with columns requested encapsulated as attributes of the model you call this method from. If you call Product.find_by_sql then the results will be returned in a Product object with the attributes you specified in the SQL query. If you call a complicated SQL query which spans multiple tables the columns specified by the SELECT will be attributes of the model, whether or not they are columns of the corresponding table."

OK, lets get to examples! I created a database on some of the characters in the CBS soap The Young and The Restless – after all, what else would serve better as a domain with complex has_many associations and relationships than a daytime soap! :D

Schema for soap:

ActiveRecord::Schema.define(version: 20160925160152) do

create_table "character_relationships", force: :cascade do |t|
  t.integer "character_b_id"
  t.integer "character_a_id"
  t.string  "relationship"
  t.string  "status"
end

  create_table "characters", force: :cascade do |t|
    t.string "first_name"
    t.string "last_name"
    t.string "description"
    t.text   "bio"
  end

  create_table "families", force: :cascade do |t|
    t.string "name"
    t.string "business"
  end

  create_table "family_characters", force: :cascade do |t|
    t.integer "family_id"
    t.integer "character_id"
  end

  create_table "relationships", force: :cascade do |t|
    t.string "type"
    t.string "description"
  end

end

Adding some data:

sqlite> select * from families;
id    name        business                                          
----  ----------  --------------------------------------------------
1     Abbott      Jabot Cosmetics                                   
2     Newman      Newman Enterprises                                
3     Winters     The Abbott - Winters Foundation                   
4     Chancellor  Chancellor Industries                             
5     Ashby       Australian Mob            

sqlite> select id,first_name,last_name,description,alive from characters;
id    first_name    last_name     description                                                             alive
----  ------------  ------------  ----------------------------------------------------------------------  ------
1     Jack          Abbott        Head of the Abbott Family since his father John Abbott died             t     
2     Billy         Abbott        The Black Sheep                                                         t     
3     Ashley        Abbott        Head Chemist of Jabot Cosmetics                                         t     
4     Victor        Newman        Head and Patriarch of the Newman Family and Newman Enterprises          t     
5     Nikki         Newman        The Woman who stands by Victor                                          t     
6     Cane          Ashby         Runaway Australian - The Character that was too popular to die          t     
7     Abby          Rayburn       Warm-hearted but spoiled daughter at the Abbott-Newman crossroads       t     
8     Phyllis       Abbott        Fierce Campaign Manager and Webmistress who goes by the nickname Red    t     
9     Nicholas      Newman        The Good Newman                                                         t  


sqlite> select * from family_characters;
id    family_id     character_id
----  ------------  ------------
1     1             1           
2     1             2           
3     4             2           
4     1             3           
5     2             4           
6     2             5           
7     3             6           
8     4             6           
9     5             6           
10    1             7           
11    2             7           
12    1             8           
13    2             8           
14    1             9        

sqlite> select * from character_relationships;
id    character_b_id   character_a_id   relationship     status      
----  ---------------  ---------------  ---------------  ------------
1     4                1                enemies          on          
2     4                8                enemies          on          
3     5                4                married          on          
4     1                8                married          complicated
5     2                8                affair           complicated
6     9                8                married          off         
7     8                9                affair           off         
8     2                1                half-siblings    complicated
9     3                7                daughter         on          
10    4                7                daughter         on          
11    3                4                affair           off         
12    5                1                affair           off         
13    5                1                friends          on          
14    3                1                siblings         on          
15    4                9                son              on          
16    5                9                son              on         

Let’s find all characters that are enemies. First using command line prompt SQL:

sqlite> SELECT first_name,last_name  from characters INNER JOIN character_relationships on characters.id = character_relationships.character_a_id OR characters.id = character_relationships.character_b_id WHERE character_relationships.relationship = "enemies";
first_name    last_name   
------------  ------------
Jack          Abbott      
Victor        Newman      
Victor        Newman      
Phyllis       Abbott   

Executing the same SQL in rails console with find_by_sql:

Character.find_by_sql 'SELECT first_name,last_name  from characters INNER JOIN character_relationships on characters.id = character_relationships.character_a_id OR characters.id = character_relationships.character_b_id WHERE character_relationships.relationship = "enemies"'

   => [#<Character id: nil, first_name: "Jack", last_name: "Abbott">, #<Character id: nil, first_name: "Victor", last_name: "Newman">, #<Character id: nil, first_name: "Victor", last_name: "Newman">, #<Character id: nil, first_name: "Phyllis", last_name: "Abbott">]

As you can see, this returns an array of model objects.

Next, we’ll use an AR query:

Character.select(:first_name,:last_name).joins(:character_relationships).where("character_relationships.relationship = ?","enemies")

 => #<ActiveRecord::Relation [#<Character id: nil, first_name: "Jack", last_name: "Abbott">, #<Character id: nil, first_name: "Phyllis", last_name: "Abbott">]>

 Character.select(:first_name,:last_name).joins(:reverse_character_relationships).where("character_relationships.relationship = ?","enemies")

 => #<ActiveRecord::Relation [#<Character id: nil, first_name: "Victor", last_name: "Newman">, #<Character id: nil, first_name: "Victor", last_name: "Newman">]>

Since we have defined the has_many and belongs_to relationship between characters as unidirectional, we have to execute the second query to find which person is on the receiving end of the animosity. Since the result type is an AR Relation in the last example, you can use AR Calculations methods on it, and, for example, pluck off the names into an array:

Character.select(:first_name,:last_name).joins(:character_relationships).where("character_relationships.relationship = ?","enemies").pluck(:first_name,:last_name)
 => [["Jack", "Abbott"], ["Phyllis", "Abbott"]]

Looks good, right? Now, let’s throw more tables into the fray and find all the families harboring animosity!

SQL command prompt:

sqlite> SELECT families.name FROM families INNER JOIN family_characters ON families.id = family_characters.family_id INNER JOIN characters ON family_characters.character_id = characters.id INNER JOIN character_relationships ON characters.id = character_relationships.character_a_id  OR characters.id = character_b_id WHERE character_relationships.relationship = "enemies" GROUP BY families.name;
  name        
  ------------
  Abbott      
  Newman          

Using AR query:

Family.select(:name).joins(:character_relationships).where("character_relationships.relationship = ?","enemies").group("name")
 => #<ActiveRecord::Relation [#<Family id: nil, name: "Abbott">, #<Family id: nil, name: "Newman">]>
 Family.select(:name).joins(:reverse_character_relationships).where("character_relationships.relationship = ?","enemies").group("families.name")
 => #<ActiveRecord::Relation [#<Family id: nil, name: "Newman">]>

Moving on…to some calculations. Remember that you have to call these methods last with AR queries. Here’s how to find the character who is a member in most of our listed families:

With RAW SQL:

sqlite> SELECT characters.first_name, characters.last_name, COUNT(families.name) AS member_of FROM characters INNER JOIN family_characters ON family_characters.character_id = characters.id INNER JOIN families ON families.id = family_characters.family_id GROUP BY characters.first_name ORDER BY member_of DESC LIMIT 1;
first_name    last_name     member_of   
------------  ------------  ------------
Cane          Ashby         3           

With AR Query and Calculations:

Character.select(:first_name).joins(:families).group("characters.first_name").order('count_families_name desc').limit(1).count('families.name')

   => {"Cane"=>3}

And lastly, we shall demonstrate .having by finding all the characters that belong to at least 2 families. Raw SQL:

sqlite> SELECT characters.first_name, characters.last_name, COUNT(families.name) AS member_of FROM characters INNER JOIN family_characters ON family_characters.character_id = characters.id INNER JOIN families ON families.id = family_characters.family_id GROUP BY characters.first_name HAVING member_of > 1;
first_name    last_name     member_of      
------------  ------------  ---------------
Abby          Rayburn       2              
Billy         Abbott        2              
Cane          Ashby         3              
Phyllis       Abbott        2     

And same in AR:

Character.select(:first_name,:last_name).joins(:families).group("characters.first_name","characters.last_name").having('count_families_name  > 1').count('families.name')
 => {["Abby", "Rayburn"]=>2, ["Billy", "Abbott"]=>2, ["Cane", "Ashby"]=>3, ["Phyllis", "Abbott"]=>2}

Weird Grammar, eh? .count returns a Fixnum, which means that you have to group your query by stating .having first: whereas in SQL, you are selecting the count so that comes first. In short summary, ActiveRecord method chaining does not completely follow the sequence of SQL queries. You have chain your methods based on the return values.

Sometimes, you’ll just have to deal with the new syntax and learn to read it right:

Resources:

http://guides.rubyonrails.org/active_record_querying.html http://api.rubyonrails.org/classes/ActiveRecord/Querying.html http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html http://api.rubyonrails.org/classes/ActiveRecord/Relation.html http://apidock.com/rails/v4.0.2/ActiveRecord/QueryMethods/having http://www.w3schools.com/sql/sql_having.asp

Class associations

class Family < ActiveRecord::Base
  has_many :family_characters
  has_many :characters, through: :family_characters
  has_many :character_relationships, through: :characters
  has_many :reverse_character_relationships, through: :characters
end    


class FamilyCharacter < ActiveRecord::Base
  belongs_to :family
  belongs_to :character
end        


class Character < ActiveRecord::Base
  has_many :family_characters
  has_many :families, through: :family_characters


  has_many(:character_relationships, :foreign_key => :character_a_id, :dependent => :destroy)
  has_many(:reverse_character_relationships, :class_name => :CharacterRelationship,
      :foreign_key => :character_b_id, :dependent => :destroy)
  has_many :characters, :through => :character_relationships, :source => :character_b

  def full_name
    "#{self.first_name} #{self.last_name}"
  end

  def full_name=(name)
    parts = name.split(" ")
    self.first_name = parts[0]
    self.last_name = parts[1]
  end      
end     


class CharacterRelationship < ActiveRecord::Base
  belongs_to :character_a, :class_name => :Character
  belongs_to :character_b, :class_name => :Character
end   

Written on September 25, 2016