Ruby on Rails: Terse, Expressive Beauty

I'm working on a little Ruby on Rails (RoR) learning project, which is just a flashcard system for practice in learning (human) languages. Nothing too terrible, but complex enough that by the time I’ve implemented my project, I’ll know RoR pretty well.

I have a nice example about why I like Ruby on Rails so much, but first I’ll have to show you these MySQL database tables from my project:

mysql> SELECT * FROM languages;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | English   |
|  2 | Français  |
|  3 | Español   |
+----+-----------+
3 rows in set (0.01 sec)

mysql> SELECT * FROM phrases;
+----+-------------+-----------------------------+
| id | language_id | content                     |
+----+-------------+-----------------------------+
|  1 |           1 | the dog                     |
|  2 |           2 | le chien                    |
|  3 |           3 | el perro                    |
|  4 |           1 | the sexually-profligate man |
+----+-------------+-----------------------------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM concepts;
+----+
| id |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

mysql> SELECT * FROM concepts_phrases;
+------------+-----------+
| concept_id | phrase_id |
+------------+-----------+
|          1 |         1 |
|          1 |         2 |
|          1 |         3 |
|          2 |         1 |
|          2 |         4 |
+------------+-----------+
5 rows in set (0.00 sec)

mysql> SELECT concept_id, phrase_id, language_id, content, languages.name
    -> FROM concepts
    -> INNER JOIN concepts_phrases ON concepts.id=concepts_phrases.concept_id
    -> INNER JOIN phrases ON concepts_phrases.phrase_id=phrases.id
    -> INNER JOIN languages ON language_id=languages.id
    -> ORDER BY concept_id, phrase_id;
+------------+-----------+-------------+-----------------------------+-----------+
| concept_id | phrase_id | language_id | phrase content              | lang name |
+------------+-----------+-------------+-----------------------------+-----------+
|          1 |         1 |           1 | the dog                     | English   |
|          1 |         2 |           2 | le chien                    | Français  |
|          1 |         3 |           3 | el perro                    | Español   |
|          2 |         1 |           1 | the dog                     | English   |
|          2 |         4 |           1 | the sexually-profligate man | English   |
+------------+-----------+-------------+-----------------------------+-----------+
5 rows in set (0.00 sec)

mysql>

Ok, so for my primitive human-language translation project (I just have to translate well enough for flashcards), each unique concept_id represents a separate idea, or concept. The tables above describe two concepts:

  1. a doggy
  2. a man who sleeps around a lot

With each concept represented by a concept_id, then various phrases in various languages can describe that concept. But there is a many-to-many relationship between concepts and phrases: in English, a dog could be the pet animal (concept 1) and or a sexually profligate man (concept 2) ["You dog, you!"], or even a homely young woman, though this part is left as an exercise for the reader.

But none of that has anything to do with Ruby — it’s just my project’s database tables.

Now, after the database above is defined, how much work did I have to do to define my models in Ruby? Here they are, in their entirety:

class Language < ActiveRecord::Base
  has_many :phrases

  validates_presence_of :name

  validates_uniqueness_of :name
end

class Phrase < ActiveRecord::Base
  belongs_to :language
  has_and_belongs_to_many :concepts

  validates_presence_of :language_id, :content

  validates_uniqueness_of :content
end

class Concept < ActiveRecord::Base
  has_and_belongs_to_many :phrases
end

That’s it, the whole thing — not a snippet. The rest of the model’s intelligence comes from the base class, ActiveRecord::Base, which learns about life by reviewing the schema of the associated database table — not too much tedious glue connecting the language to the database!

The whole language is as terse and expressive:

Suppose that for the tables above, you want to see the content field, 'phrase.content', for all of the phrases, in all of the languages, that are part of concept #1? I'll execute it here, from the ruby console, right before your eyes:

>> concept1 = Concept.find(1)

Reply:
=> #<Concept:0x349fdec @attributes={"id"=>"1"}>

RoR looked up the record for concept #1 in the database, and returned a Ruby object. Boring, I know. But what about that concept’s phrases’ phrase.content values?

>> concept1.phrases.collect { |ph| ph.content }

Reply:
=> ["the dog", "le chien", "el perro"]

And there we are, the phrases for concept #1.

That’s…so cool! In those first few words, concept1.phrases, rails did a join on three database tables:

    concepts X concepts_phrases X phrases

…and plucked out the matching phrases for the concept.

The rest of the line means, “Make a new list, working from each phrase in the old list. For each phrase ph in the old list, make the new list have ph.content, the content field for that phrase.

What’s that? You’re a little sad that we don’t have the name of the language for each phrase? The phrase rows have the language_id, but we want language.name, which is in a whole separate table -- won’t that be a lot of code? No, of course not! It’s only about another 20 characters to make [phrase.content, language.name] pairs:

>> concept1.phrases.collect { |ph| [ph.content, ph.language.name] }

Reply:
=> [["the dog", "English"], ["le chien", "Français"], ["el perro", "Español"]]

There, an array of arrays, with just the few things we wanted. We did joins on four tables:

    concepts X concepts_phrases X phrases X languages

…and extracted just what we wanted, in darn few characters.

You can't say that isn't cool, because it is cool.

Comments

  1. Bill Standley wrote:

    That is cool! Thanks for the RonR tour, Tom!

  2. John Blackburn wrote:

    The code samples really made this post fund to read. There’s something about code samples that, as soon as you see them, you go, ohhh, and read those first, like pictures in a book. “Oh look, a doggie!”.

    *Then* you read the context.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*

*