Quotes. Data. Models.
in order from most to least sexy.
![This^ or "An eye for eye makes the whole world blind." You be the judge.](https://miro.medium.com/1*Nz5w0e2nMuxD4ylxJmm7UA.jpeg)
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
Add 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
Then!:
$ rake db:migrate
Next, I'll add a mirror of the db restrictions to the two new models:
# topic.rb
validates_presence_of :title
# category.rb
validates_presence_of :title
Associations
And add the new associations to the three models:
# quote.rb
belongs_to :topic
# topic.rb
has_many :quotes
belongs_to :category
# category.rb
has_many :topics
has_many :quotes, through: :topics
Lifecycle
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.
# quote.rb
after_save :ensure_topic
def ensure_topic
if !self.topic || !self.category
self.topic = Topic.uncategorizedTopic
end
end
# topic.rb
def self.uncategorizedTopic
topic = self.find_by(title: 'uncategorized')
topic ? topic : create_uncategorized_topic
end
def create_uncategorized_topic
category = Category.find_or_create_by(title: 'uncategorized')
Topic.create(title: 'uncategorized', category: category)
end
I'll also add a before_save
to both Topics and Categories that downcases their titles:
# topic.rb
before_save :downcase_title
# category.rb
before_save :downcase_title
# both individually
def downcase_title
self.title = self.title.downcase
end
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... ✔
Great.
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...
Writing 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:
# quote.rb
def category
topic.category
end
Smell ya later!
Written: December 19, 2017
Last updated: January 12, 2025