A Base Model

Inheriting From a Base Model

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:

  1. variable names e.g. post_hash
  2. class names e.g. Post in Post.new
  3. table name strings e.g. 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.