Let’s leave the modeling to the data.
Last we left, the quotes API was able to both be told to create a quote and call for a single quotation. As great as that is, I would like to be able to add more information to the quote in a maintainable way.
The book that I will be using to source quotes also provides a category and sub-category (I’ll call them “topics”) for each quote.
For any one category, there are many topics. For any one topic, there are many quotes. For any one quote, there is only one topic and category.
In object relational terms, category:topics are one:many, topics:quotes are one:many. A quote will be able to tell its category based on its topic.
A title on a category will be something like “making dreams come true”, and some of its topics will be “instinct” and “luck”.
Since there are two new resources, I generate two new models.
$ rails g model topic title:string category_id:integer
$ rails g model category title:string
null: false to the title attribute in both migration files.
Next, I add the
topic_id attribute to the quotes table.
$ rails g migration AddTopicIdToQuotes topic_id:integer
$ rake db:migrate
Next, I’ll add a mirror of the db restrictions to the two new models:
validates_presence_of :title# category.rb
And add the new associations to the three models:
belongs_to :topic# topic.rb
belongs_to :category# category.rb
has_many :quotes, through: :topics
I’ve extensively considered making the quote and topic association optional. For my own purposes, I will not be using the information, so whether it’s
nil or a true
Topic object make no difference. But to make a Quote’s API more uniform, I’ve decided to add an
after_save lifecycle hook to always save a Topic/Category combo to a Quote.
after_save :ensure_topicdef ensure_topic
if !self.topic || !self.category
self.topic = Topic.uncategorizedTopic
topic = self.find_by(title: 'uncategorized')
topic ? topic : create_uncategorized_topic
category = Category.find_or_create_by(title: 'uncategorized')
Topic.create(title: 'uncategorized', category: category)
I’ll also add a
before_save to both Topics and Categories that downcases their titles:
before_save :downcase_title# category.rb
before_save :downcase_title# both individually
self.title = self.title.downcase
I’ll hop into the rails console to make sure everything is working:
$ rails c
$ > quote = Quote.create(content: 'yolo', attribution: 'Alexa')
$ > quote.topic
$ => #<Topic id: 3, title: "uncategorized", ...
$ > quote.topic.category
$ => #<Category id: 4, title: "uncategorized">
Uncategorized defaults… ✔
$ category = Category.create(title: 'ALL CAPS')
$ => #<Category id: 5, title: "all caps">
$ topic = Topic.create(title: 'MiXeD cAsE', category: category)
$ => #<Topic id: 4, title: "mixed case", category_id: 5...
Lowercased titles… ✔
That’s the extent of the resources I plan to have in the quotes API phase one. As you’ll see, I’ll actually be exposing only one of the three resources. Check out the next article where I make a dead simple user interface for creating and querying for quotes.
Until then, check out the repository branch for this post. Oh…
quote.topic.category every time I need to access a quote’s category is a bummer. Here’s a quick helper getter that solves that:
Smell ya later!