Now that we've completed work on our Comment
model, we've seen tons of duplication between it and our Post
model. In particular, there are several methods that are identical between the two models, such as:
#new_record?
#save
.connection
and #connection
So let's pull these methods out of these two models and place them inside a new model:
### app/models/base_model.rb ###
class BaseModel
attr_reader :errors
def new_record?
@id.blank?
end
def save
return false unless valid?
if new_record?
insert
else
update
end
true
end
def self.connection
db_connection = SQLite3::Database.new 'db/development.sqlite3'
db_connection.results_as_hash = true
db_connection
end
def connection
self.class.connection
end
end
This BaseModel
model class holds the shared functionality of both of our prior models, so we can now completely remove those methods from Post
and Comment
, if we change both of them to inherit from BaseModel
:)
### app/models/post.rb ###
class Post < BaseModel
# ...
end
### app/models/comment.rb ###
class Comment < BaseModel
# ...
end
BaseModel
nicely consolidates a fair amount of common logic for us, but there a few methods that are almost the same in both classes: .all
, .find
and #destroy
They're not exactly the same. Let's take a look at .all
:
### app/models/post.rb ###
class Post
# ...
def self.all
post_hashes = connection.execute("SELECT * FROM posts")
post_hashes.map do |post_hash|
Post.new(post_hash)
end
end
# ...
end
### app/models/comment.rb ###
class Comment
# ...
def self.all
comment_hashes = connection.execute("SELECT * FROM comments")
comment_hashes.map do |comment_hash|
Comment.new(comment_hash)
end
end
# ...
end
Notice that what these methods are doing is the same, but things are named differently. In particular, the naming differences are as follows:
post_hash
Post
in Post.new
posts
in SELECT * FROM posts
We'll address each of these, as we work to move this method into BaseModel
to be shared by Post
and Comment
.
The issue of the variable names is simple: we'll pick something appropriate, but more generic. Taking post_hashes
as an example, we could instead call the variable record_hashes
.
### app/models/base_model.rb ###
class BaseModel
# ...
def self.all
# rename all the variables...
record_hashes = connection.execute("SELECT * FROM posts")
record_hashes.map do |record_hash|
Post.new(record_hash)
end
end
# ...
end
Next, the use of class names with the Post.new
and Comment.new
calls are actually syntactically unnecessary. We can omit the classes and call new
all by itself. The reason for this is that .all
is a class method. So if we call new
inside of this class method, it will be called on the class it lives in.
### app/models/base_model.rb ###
class BaseModel
# ...
def self.all
record_hashes = connection.execute("SELECT * FROM posts")
record_hashes.map do |record_hash|
new record_hash
end
end
# ...
end
And lastly there's the matter of the table names. Inside Comment
, a query should be made to the comments
table, and in Post
, posts
.
Notice the transformation there?
Post
goes to posts
and Comment
goes to comments
. It would be nice if our .all
method could look at the class it lives in and figure out what to do based on the name of that class. Fortunately for us, this is not only common, but also easy in Ruby.
Let's see how we can get from a class (a constant) to the string we want:
Post
# => Post
Post.to_s
# => "Post"
Post.to_s.pluralize
# => "Posts"
Post.to_s.pluralize.downcase
# => "posts"
Let's give our classes a .table_name
method to figure out the appropriate table name string and then make use of it inside of .all
:
### app/models/base_model.rb ###
class BaseModel
# ...
# a way to get the right table name string
def self.table_name
to_s.pluralize.downcase
end
def self.all
# use it in our SQL
record_hashes = connection.execute("SELECT * FROM #{table_name}")
record_hashes.map do |record_hash|
new record_hash
end
end
# ...
end
It's worth noting that our call got shortened to to_s.pluralize.downcase
inside of .table_name
. The reason for this is the same as our shortened call to new
earlier: inside of a class method self
is the class.
Based on what we've done with our .all
class method, we can also extract .find
, and #destroy
to the base model as well.
class BaseModel
# ...
def destroy
query_string = "DELETE FROM #{self.class.table_name} WHERE #{self.class.table_name}.id = ?"
connection.execute query_string, id
end
def self.find(id)
query_string = "SELECT * FROM #{table_name} WHERE #{table_name}.id = ? LIMIT 1"
record_hash = connection.execute(query_string, id).first
new(record_hash)
end
# ...
end
And with that, we can remove .all
,#destroy
, .find
from the Comment
and Post
models. Since both classes inherit from BaseModel
, they will now each make use of this new implementation.
This process of having an object look at itself is called introspection. You will see it and its effects littered all throughout Rails.
We've come a long way, soon it will be time to start implementing Rails conventions in our app. In the next section, we will slowly introduce Rails conventions (magic) that can help make the work we've been doing simpler, easier to read, and more efficient.