Learn Ruby and Rails by building your own very simplistic Rails-like Framework.
We'll start by briefly defining seven terms and concepts: resources, database CRUD functions, HTTP methods, RESTful web architecture, the MVC model, active record architecture, and ORM.
Resource: A web resource is anything that can be obtained from the World Wide Web. Indeed the web address you type in your browser is called Uniform Resource Locator (URL). Rails uses the term resource more broadly to mean a RESTful resource, with a database table and the corresponding model, controller, and set of routes. We'll adopt that definition as well. In our Blog application we will create an Article resource that will include a database table called articles, a model class called Article, a controller class called ArticlesController, web page templates in an app/views/articles directory, and a set of articles routes.
CRUD: Most web applications need to persist data, generally by saving it to a database. On the database side this is often referred to as a CRUD (Create-Read-Update-Delete) application. In our database we will create a table called articles. This will give us the ability to create new articles (records) and save them in the database, read them from the database to display on our site, update and delete them.
HTTP Methods: The World Wide Web uses the Hypertext Transfer Protocol (HTTP) to exchange or transfer data (hypertext) between our users' computer/web browser (the client) and our website's server. HTTP methods POST, GET, PUT or PATCH, and DELETE correspond with the database CRUD functions. When a user enters a web address (url) in their browser it sends a GET request through the internet to our server to view a specific page. The page they request may or may not require data from a database. If it is to our articles page then our application will need to read from the articles database table to display a list of articles. If the user fills out a new article form and hits the submit button, their browser sends a POST request that sends the form data. Our application will take that POST request and create a new record in the database. A user can also send a request to update or delete an article, although those aren't necessarily done with HTTP PUT/PATCH and DELETE methods. Rails handles those with HTTP POST and GET requests respectively, along with a hidden form field indicating an update and a data attribute indicating a delete. The HTTP PUT method is to update an entire record with new data while the PATCH method is to update only specific fields.
REST (REpresentational State Transfer): REST is an architectural style for a web application. The term came from a 2000 Doctoral dissertation by Roy Fielding. A "RESTful" web application has a network of Web resources that the user progresses through by selecting links to perform GET, POST, PUT/PATCH, and DELETE operations. Each URL and operation represents a different state. Rails structures it's default REST architecture around a resource and seven routes and controller actions: index, show, new, edit, create, update, and destroy. Each of these represents a separate state. In a Rails application with an Article resource, a user can view an index page of all articles, view a show page of a specific article, access a new or edit form, create or update an article by submitting the form, and finally destroy an article with the delete button or link.
Active Record Pattern: Is a software architecture pattern for accessing data in a database from an object-oriented programming language (like Ruby). A database table is wrapped into a class. Each instance of that class is tied to a corresponding row in the database table. After creating an object of the class, a corresponding row is added to the table upon save. When accessing an existing object, that object gets it's information from the database. When updating an object, the corresponding row in the table is also updated. And when deleting an object, the corresponding database table's row is deleted. The wrapper class implements accessor methods or properties for each column in the table.
Model: The model is the part of the Model-View-Controller application architecture that represents the data. In Rails each resource is represented by a model. You instantiate an instance of the model class every time your application applies a RESTful action on a resource. When you create a new article, you instantiate an object (an instance) of the Article class. Your model then saves the object as a new record in your database following the Active Record Pattern, using an ORM library.
ORM (Object-relational Mapping) library: ORM is a technique that lets you interact with a database to perform CRUD operations from an object-oriented programming language. Our web application will have an Article resource. When a user submits a new article form, we instantiate a new article object in the model's Article class. Then we use an ORM library to save that object as a new record in our database. Rails is composed of ten separate but interconnected gems, one of which is an ORM library called ActiveRecord which Rails model classes inherit from. Since ActiveRecord is packaged as it's own gem, it can be used in non-Rails Ruby applications. Conversely, Rails applications can use an ORM library other than Active Record, such as the Sequel gem or the DataMapper gem.
We will use a simple Active Record Pattern on our application and framework. For each resource our app will include a model file with a class named after it. The model classes will inherit from an ORM module in our Jails library that we'll call HacktiveRecord. We'll use a Sqlite database. And we'll employ a RESTful controller and set of routes and views around each resource. For our Blog application we will create an Article resource.
Create the file structure for our Article resource. That includes a controller, views, a model file, and some migration files to create the database and seed it with some articles. We already created a file in our Jails MVC framework library for simplistic ORM library lib/jails/hacktive_record.rb.
touch app/controllers/articles_controller.rb mkdir app/views/articles touch app/views/articles/edit.html.erb touch app/views/articles/index.html.erb touch app/views/articles/new.html.erb touch app/views/articles/show.html.erb touch app/models/article.rb touch db/migrate/001_create_articles.rb touch db/migrate/002_article_seeds.rb
Like Rails, we will be using the Active Record Pattern for persisting instances of our resource articles. That entails instantiating article objects in memory from an Article class that correspond to records in an articles database table. So let's start with the in memory article objects without persistence.
A Struct (short for structure) is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class. Open IRB in the terminal irb
and create a structure for articles and assign it to a constant.
Article = Struct.new(:id, :title, :content, :created_at) rack_article = Article.new(1, "Learn Rack", "Lorem Ipsum", Time.now) puts(rack_article) rack_article.title = "Learn Routes" rack_article.title object = Article.new(2, "Learn Models", "Lorem Ipsum", Time.now) puts(object) object.title
Let's work with an Article class. Populate the article.rb file with an empty Article class.
# app/models/article.rb class Article end
Close then close your IRB session (Control+D) reopen IRB (irb
) from the Blog project folder to clear out the Article constant. Load the article.rb file to get access to the class. Then instantiate a new Article object by calling the new method on the Article class. New is a special Ruby method, called a constructor, that creates a new object. We'll assign the object to a local variable called object1. You can't use dashes in variable names, object-1 for example, because Ruby will try to subtract 1 from a variable named object.
load 'app/models/article.rb' object1 = Article.newThis will create an Article object in you computer's RAM memory and return a string that shows the class name and an encoding of the object's id. But there is no way to add attributes to this object like id, title, content and created_at. If you tried
object1.id = 1
you would get an error saying the id method is undefined. We need to add Ruby getter and setter methods to the class to allow us to set specific attributes such as title, and get the attribute value back. Ruby has a shortcut method called attr_accessor. In the article.rb file, just below the Article class definition add this line:
attr_accessor(:id, :title, :content, :created_at)
Now we will be able to set and get values for the attribute symbols provided. The attribute will act like a method that we chain to the object. We'll start by reloading the article.rb file to have the changes available to us. object1.id = 1
will set the object's id attribute value to 1, and object1.id
will return the the id attribute's value. If we only wanted to give write access we would use attr_writer
and for read-only access attr_reader
. Now assign the following attributes to the object. For the content attribute, we'll just use the Lorem Ipsum filler text for the value. We'll call the now method on the Time class to get the current date and time.
load 'app/models/article.rb' object1.id = 1 object1.title = "Learn Controllers" object1.content = "Lorem Ipsum" object1.created_at = Time.now object1
Now if you enter object1
you'll see that the object has all the attributes that you assigned it, set as instance variables.
If we want to set the attributes at the same time we instantiate the object, then we need to define an initialize method in the class. Initialize is another special Ruby method. When you instantiate an object with new, Ruby will automatically call the initialize method. If there is no initialize method Ruby will create the object without it as we've seen. But if you want to set attributes or run methods when a new object is created, then you need to add an initialize method.
In the Article class, below the attr_accessor method, define the initialize method. Add each attribute as an argument with default values (which can be nil or an empty string ''). But like all methods, you have to do something with the arguments that are passed in, otherwise they are ignored. Assigning the arguments to instance variables will make them object attributes.
def initialize(id: nil, title: nil, content: 'TBD', created_at: Time.now) @id = id @title = title @content = content @created_at = created_at end
Another way to do it is to use a splat parameter. That allows any number of arguments to be received. Then set the instance variables to the attribute key. We will send the arguments in a hash and the keys will be symbols.
def initialize(*attributes) @id = :id @title = :title @content = :content @created_at = Time.now end
And a third way to do it is to set a single parameter set to a hash that will hold the submitted attributes. We're using the || or operator to add a default in case the attribute key is not present.
def initialize(attributes = {}) @id = attributes[:id] || '' @title = attributes[:title] || '' @content = attributes[:content] || 'TBD' @created_at = Time.now end
Reload the file using any one of the above formats and add a new record with a hash of attributes as the argument and you should see an object created with the attributes.
load 'app/models/article.rb' object2 = Article.new(id:2, title:"Learn Classes", content:"Lorem Ipsum")
Finally, lets add some methods to our class. Use the code below:
# app/models/article.rb class Article attr_accessor(:id, :title, :content, :created_at) def initialize(title: nil, content: 'TBD') if defined?(@@article_count) @@article_count += 1 else @@article_count = 1 end @id = @@article_count @title = titleize(title) @content = content @created_at = timestamp end def self.count @@article_count end def timestamp Time.now.to_s end def titleize(string) do_not_capitalize = ["the","a","an","and","but","or","nor","for","of","to","at","by","from","in","on"] array = string.split.each do |word| if do_not_capitalize.include?(word.downcase) word.downcase! else word.capitalize! end end array[0].capitalize + " " + array[1..-1].join(" ") end def to_s title end end
In the initialize method we'll declare a class variable called @@article_count that increments by 1 every time a new Article object is instantiated. Our conditional checks if the variable has been defined yet. If so it increments it by 1, and if not it defines it and sets it to 1. And we will use that variable to set the id value. We'll also create a class method called self.count that returns the @@article_count value. That is because class variables can't be accessed directly from outside the class, but a class method that returns the value can. Class variables and class methods are applied to the class and not to an instance of the class. Class methods begin with the keyword self which means the class itself. You could replace self.count with Article.count and it will work just the same.
We'll create an instance method, meaning it is applied to a specific Article instance/object, called timestamp. This will return the current date and time and convert it to a string. We'll call that when setting the created_at attribute.
We'll define an instance method that will titleize a string, capitalizing the first letter of each word, less a provided word list. The first word in the title is always capitalized. We'll use that to titleize article titles when a new article is instantiated.
The last instance method is an override of the Ruby to_s method when applied to an Article object. To_s converts the receiver object to a string. By default, when the object is called, Ruby returns a string that includes the class name, the encoded object id, and the attributes. But with this override, when an Article object is called with to_s it will return the title.
Start a new session of IRB to clear the other objects from memory (Control+D irb
). Load the article.rb file and create three objects.
load 'app/models/article.rb' object1 = Article.new(title: "learn controllers", content: "Lorem Ipsum") object2 = Article.new(title: "LEARN VIEWS", content: "Lorem Ipsum") object3 = Article.new(title: "learn modELS", content: "Lorem Ipsum")
You can modify the objects:
object2.title = "Learn Routes"
object2
The objects are stored in RAM memory and so you don't need to delete them.
You can call a class method to get the number of article objects:
Article.count
We overrode the to_s method so calling it will give you the object's title:
object2.to_s
The puts method will also return the object as a string puts object2
.
There are two sides to the Active Record pattern. We just covered in-memory objects, now we'll cover persistence in a database. Like Rails we'll default to a SQLite database for development. It is light-weight and it actually gets embedded directly into our program. We placed it in the db folder. SQLite generally isn't used in production except for smaller applications. PostgreSQL and MySQL by contrast employ a client-server setup and are production ready. With Rails you can use those databases in production as well as development simply by adding the relevant gem to your gemfile and Rails will change the configuration. We won't be quite that automated, but you can apply this code to those databases as well with minimal modifications to the code.
Reference: rubygems.org/gems/sqlite3 | Documentation
Sqlite3 is a library, packaged as a gem, that lets Ruby programs interface with the SQLite database engine. The SQLite database is currently on version 3, so this gem is compatible with that version. It provides methods for interacting with the database that we will use in our code.
Add the sqlite3 gem:
# Gemfile ... # Allow Ruby programs to interface with the lightweight SQLite3 embedded database. gem "sqlite3"
bundle install
Let's create a database and add an articles table to it with the necessary columns. We'll use a migration file to do it like Rails does. But unlike Rails, we don't have generators so we'll create it manually.
We need to require sqlite3 to access it's Database class. The database file will be created in the db folder. The database name can be anything but we'll use the same name that Rails uses, development.sqlite3.
Then we'll execute an SQL statement to create the table. If the database does not already exist, it will be created when the table is created. The SQL statement is in a multi-line string delimited with a here document <<SQL...SQL. Add the data type after the column name. TEXT data type is for large amounts of text. In SQLite VARCHAR(n) and TEXT are both treated as TEXT datatype. In MySQL VARCHAR(n) can hold up to 255 characters. In Postgresql VARCHAR(n) can hold up to the amount you specify.
# db/migrate/001_create_articles.rb require 'sqlite3' # define db. If there is no db/development.sqlite3 it will be created. db = SQLite3::Database.new(File.join("db", "development.sqlite3")) db.execute(<<SQL CREATE TABLE articles ( id INTEGER PRIMARY KEY, title VARCHAR(255), content TEXT, created_at DATETIME DEFAULT NULL ); SQL ) # To run this: $ ruby db/migrate/001_create_articles.rb # To see if the db file is created in the db folder: $ ls db
Make sure you are out of IRB (Control+D or exit
). Then run the migration from the terminal.
ruby db/migrate/001_create_articles.rb
To see if the db file is created in the db folder, enter the UNIX list command for the db folder.
ls db
Now let's seed the database with some articles. Rails uses a db/seeds.rb file for all seeds. We'll vary from that and just make it another migration file.
# db/migrate/002_article_seeds.rb require 'sqlite3' db = SQLite3::Database.new(File.join("db", "development.sqlite3")) db.execute(<<SQL INSERT INTO articles(title, content, created_at) VALUES('Learn Rack', 'Lorem Ipsum', CURRENT_TIMESTAMP), ('Learn Models', 'Lorem Ipsum', CURRENT_TIMESTAMP); SQL ) # To run this: $ ruby db/migrate/002_article_seeds.rbThen run the migration from the terminal.
ruby db/migrate/002_article_seeds.rb
The Sqlite3 gem provides a shell that you can also access from the terminal and run SQL statements on it directly. To confirm that sqlite3 is installed, call the version sqlite3 --version
.
Then enter the shell and connect to the database:
sqlite3 db/development.sqlite3
Then you can enter your SQL statement(s) directly. Let's do a full series of CRUD database entries (Create, Read, Update, Delete) that will line up with our RESTful controller actions (new, create, index, show, edit, update, destroy).
First let's create a new article.
INSERT INTO articles (title, content, created_at) VALUES ('Learn SQL', 'Lorem Ipsum', CURRENT_TIMESTAMP);
Next Read all all articles records. You should see the two records you created with the seed file migration, plus the record you just created in the shell.
SELECT * FROM articles;
Now read the article record with ID 3. We are adding a limit of 1 so the database stops looking after the first item is found.
SELECT * FROM articles WHERE id = 3 LIMIT 1;
Update the title of article id 3 to "Learn Storage", then read it to make sure it changed.
UPDATE articles SET title = 'Learn Storage' WHERE id = 3 LIMIT 1;
SELECT * FROM articles WHERE id = 3 LIMIT 1;
Delete article 3.
DELETE FROM articles WHERE id = 3 LIMIT 1;
Now read all articles records and record 3 should be gone.
SELECT * FROM articles;
Exit the Sqlite3 shell with Control+D
Let's enter the same type of SQL commands but using Ruby code in IRB. Enter the IRB shell irb
. Then enter the commands below:
require "sqlite3" db = SQLite3::Database.new("db/development.sqlite3") db.execute("INSERT INTO articles (title, content, created_at) VALUES (?,?,CURRENT_TIMESTAMP);" 'Learn SQL', 'Lorem Ipsum') db.execute("SELECT * FROM articles;") db.execute("SELECT * FROM articles WHERE id = ? LIMIT 1;", 3) db.execute("UPDATE articles SET title = ? WHERE id = ?;", 'Learn ORM', 3) db.execute("SELECT * FROM articles WHERE id = ? LIMIT 1;", 3) db.execute("DELETE FROM articles WHERE id = ?;", 3) db.execute("SELECT * FROM articles;")
1) Require the sqlite3 gem.
2) Set a variable db to a new Sqlite database connection.
3-9) Chain Sqlite3's execute method with the db connection as the receiver. The argument is a string containing the SQL statement you want to execute. Then perform the CRUD functions with SQL statements. Notice the SQL statement values have question mark placeholders, with the values given after the statement. This is called a prepared statement and the variables are called bind variables. Prepared statements are used to prevent SQL injection attacks where a malicious user enters SQL code in your forms to mess with your database. With the prepared statements the inputs are sanitized, meaning any SQL code is escaped before being sent to the database.
Let's create an Article resource. We'll make it RESTful in the way Rails does, with seven routes and controller actions, and views to display all our articles, or a particular article, and forms to create and edit articles.
Add the routes:
# config/routes.rb ... match("/articles", "articles#index") match("/articles/new", "articles#new") match("/articles/create", "articles#create") match("/articles/:id", "articles#show") match("/articles/:id/edit", "articles#edit") match("/articles/:id/update", "articles#update") match("/articles/:id/destroy", "articles#destroy") ...
Add the controller. The RESTful controller actions follow closely to the Rails default controller actions.
# app/controllers/articles_controller.rb class ArticlesController < Controller # GET /articles def index @articles = Article.all render end # GET /articles/1 def show @article = Article.find(params[:id]) render end # GET /articles/new def new render end # POST /articles/create def create @article = Article.new(title: params['article']['title'], content: params['article']['content']) @article.save redirect_to "/articles/#{@article.id}" end # GET /articles/1/edit def edit @article = Article.find(params[:id]) render end # PATCH /articles/1/update def update @article = Article.find(params[:id]) @article.update(title: params['article']['title'], content: params['article']['content']) redirect_to "/articles/#{@article.id}" end # GET /articles/1/destroy def destroy @article = Article.find(params[:id]) @article.destroy redirect_to("/articles") end end
Add simple views.
# app/views/articles/index.html.erb <!DOCTYPE html> <html> <%= render_partial("layouts/_head.html.erb") %> <body> <div id="container"> <%= render_partial("layouts/_navbar.html.erb") %> <h1>Articles</h1> <p> <%= @articles.size %> Articles | <a href="/articles/new">Create a new article</a></p> <hr> <% @articles.each do |article| %> <h3><a href="/articles/<%= article.id %>"><%= article.title %></a></h3> <div><%= article.content %></div> <hr> <% end %> </div> </body> </html>
# app/views/articles/show.html.erb <!DOCTYPE html> <html> <%= render_partial("layouts/_head.html.erb") %> <body> <div id="container"> <%= render_partial("layouts/_navbar.html.erb") %> <h1><%= @article.title %></h1> <!-- Created_at date is saved in the database as a string. Convert it to a date object before formatting it. --> <p><%= DateTime.parse(@article.created_at).strftime("%b %d, %Y") if @article.created_at %></p> <p> <a href="/articles">Back to the list</a> | <a href="/articles/<%= @article.id %>/edit">Edit</a> | <a href="/articles/<%= @article.id %>/destroy">Delete</a> </p> <hr> <p> <%= @article.content %> </p> </div> </body> </html>
# app/views/articles.new.html.erb <!DOCTYPE html> <html> <%= render_partial("layouts/_head.html.erb") %> <body> <div id="container"> <%= render_partial("layouts/_navbar.html.erb") %> <h1>New Article</h1> <hr> <form action="/articles/create" method="POST"> <div> <label for="title">Title</label><br> <input type="text" name="article[title]" id="title" autofocus> </div> <div> <label for="content">Content</label><br> <textarea rows="10" name="article[content]" id="content"></textarea> </div> <input type="submit" name="commit" value="Send"> </form> </div> </body> </html>
# app/views/articles/edit.html.erb <!DOCTYPE html> <html> <%= render_partial("layouts/_head.html.erb") %> <body> <div id="container"> <%= render_partial("layouts/_navbar.html.erb") %> <h1>Edit Article</h1> <hr> <form action="/articles/<%= @article.id %>/update" method="PATCH"> <div> <label for="title">Title</label><br> <input type="text" name="article[title]" id="title" value="<%= @article.title %>"> </div> <div> <label for="content">Content</label><br> <textarea rows="10" name="article[content]" id="content"><%= @article.content %></textarea> </div> <input type="submit" name="commit" value="Send"> </form> </div> </body> </html>
Okay, now let's create our own simplified Object-Relational Mapping library for our application, and apply it to an articles resource. We'll set up our ORM so that it can be applied to any RESTful resource in our app. And when we separate our Jails framework code to a separate gem, it can be applied to other applications as well.
We already created the database table and added some records. We also populated the model file, but let's modify it. We'll inherit from HacktiveRecord::Base. We can get rid of the @@article_count class variable since we can get our count from the database. We'll get rid of the titleize method but we'll see it again later.
# app/models/article.rb class Article < HacktiveRecord::Base # Getter and Setter methods for listed attributes. attr_accessor(:id, :title, :content, :created_at) # Set attributes when a new article object is instantiated. def initialize(id: nil, title: nil, content: nil, created_at: timestamp) @id = id @title = title @content = content @created_at = created_at end # Return current date and time as a string. def timestamp Time.now.to_s end # Return title when accessing object as string, overrides default: class, encoded object id and instance variables. def to_s title end end
When initializing a new Article we need to get the current date and time for the created_at attribute. We define a timestamp method to do that, and convert it to a string so it can be saved in the database. When we want to read the created_at value, we define a created_at method to convert it back to a date object.
The hactive_record.rb file will be our Framework's ORM, interacting with the database. This code can apply to any resource that follows the same RESTful structure as our Articles resource. We would simply create another model class such as Comment or User, and inherit from HacktiveRecord::Base. We are making HacktiveRecord a module to give it a separate namespace into which we could put multiple classes. We just have one class called Base, but we could break out different classes for different types of databases that Base would inherit from. One for SQLite, one for PostgreSQL, one for MySQL for example, but we'll keep it simple and not do that.
# lib/jails/hacktive_record.rb require "sqlite3" # Simplistic ORM library using the Active Record pattern module HacktiveRecord class Base DB = SQLite3::Database.new("db/development.sqlite3") # Return table name string by transforming the model class's name to lower case plural. def self.table table_name = self.name.downcase + "s" return table_name end # Return array of DB column names converted to symbols. def self.columns columns = DB.table_info(table).map { |info| info["name"].to_sym } return columns end # Return number of rows by executing a count query on the database for the resource. def self.count rows_count = DB.execute("SELECT COUNT(*) FROM #{table}")[0][0] puts("\s #{self} SQL Statement: SELECT COUNT(*) FROM #{table}") return rows_count end # Return array of all rows in queried from the database table, converted to objects of the resource. def self.all rows = DB.execute("SELECT * FROM #{table}") puts("\s #{self} SQL Statement: SELECT * FROM #{table}") objects = rows.map do |row| attributes = Hash[columns.zip(row)] self.new(attributes) end return objects end # Return an object by querying the database for the requested row searching by id. def self.find(id) row = DB.execute("SELECT * FROM #{table} WHERE id = ? LIMIT 1", id).first puts("\s #{self} SQL Statement: SELECT * FROM #{table} WHERE id = #{id} LIMIT 1") attributes = Hash[columns.zip(row)] object = self.new(attributes) return object end # Save object as a new row to the database table, returning the object with the new attribute's id value. def save new_object = self columns = new_object.class.columns columns.delete(:id) placeholders = (["?"] * (columns.size)).join(",") values = columns.map { |key| self.send(key) } columns = columns.join(",") DB.execute("INSERT INTO #{self.class.table} (#{columns}) VALUES (#{placeholders})", values) puts("\s #{self.class} SQL Statement: INSERT INTO #{self.class.table} (#{columns}) VALUES (#{placeholders})" + values.to_s) new_object.id = DB.execute("SELECT last_insert_rowid()")[0][0] return new_object end # Modify row in database. def update(attributes={}) columns = attributes.keys columns = columns.map { |column| "#{column}=?" }.join(",") values = attributes.values values << id DB.execute("UPDATE #{self.class.table} SET #{columns} WHERE id = ?", values) puts("\s #{self.class} SQL Statement: UPDATE #{self.class.table} SET #{columns} WHERE id = ?" + values.to_s) end # Delete row from database. def destroy DB.execute("DELETE FROM #{self.class.table} WHERE id = ?", id) puts("\s #{self.class} SQL Statement: DELETE FROM #{self.class.table} WHERE id = #{id}") end end end
Let's break it down:
We installed the sqlite3 gem, so at the top we will require it to access the SQLite3 module.
DB = SQLite3::Database.new("db/development.sqlite3")
At the top of our Base class declare a constant called DB that will instantiate a new connection to our database. SQLite embeds the database in our app, wherever we want to put it. We have a db folder so we put it there. We can name the database whatever we want, but we're calling it development.sqlite3.
def self.table table_name = self.name.downcase + "s" return table_name end
Our first method is a class method that returns the name of the table. It is derived from the class name (represented by the keyword self), put in lowercase letters and made plural by adding an s. This is convention over configuration. We won't get fancy and worry about irregular plurals like Person-people. If your class name is Article, your table name will be articles.
def self.columns columns = DB.table_info(table).map { |info| info["name"].to_sym } return columns end
The columns class method connects to the database, then calls the SQLite table_info method which returns information about each column including the column name. We apply the map method which will iterate through each item and apply a block to it, returning an array. In our block we'll take the "name" attribute from each column and convert it to a symbol. The columns method thus returns an array of all the column names from our table, converted to symbols.
def self.count rows_count = DB.execute("SELECT COUNT(*) FROM #{table}")[0][0] puts("\s #{self} SQL Statement: SELECT COUNT(*) FROM #{table}") return rows_count end
The count class method will perform a database query and return the number of records for the table. And we'll log the SQL query to the terminal with a puts.
When a user of our blog application clicks on the Articles link it goes to the /articles path. This is routed to the articles controller's index action.
def index @articles = Article.all render endThis calls the all method from our model's Article class (defined in our app/models/article.rb file). Except it's not defined in the Article class, it's defined in the HacktiveRecord::Base class which Article inherits from. But it works the same as if it was defined directly in the Article class, as do all the constants and methods defined there. Notice Article is the receiver of the all method. Article is a class so the all method is a class method prefaced with the keyword self.
def self.all rows = DB.execute("SELECT * FROM #{table}") # 1 puts("\s #{self} SQL Statement: SELECT * FROM #{table}") # 2 objects = rows.map do |row| # 3 attributes = Hash[columns.zip(row)] self.new(attributes) end return objects # 7 end
1) Execute a database query to get all the rows from our table. Each row contains an array of values from an individual article.
2) Log the SQL statement to the terminal using puts.
3-6) Use a block to convert the row values into Ruby objects. The map method returns an array of objects, one for each article. To get an object we use the zip method from the Hash class. It combines the array of columns that we get from the columns method, with the array of row values, giving us key value pairs. Then we call the new method on self (self representing Article), which creates a new Article object.
7) Return the array of article objects.
So the Article.all method call returned an array of objects for all the articles in the database. This was assigned in the controller to the @articles instance variable. Then on the index.html.erb view page, @articles is accessed to iterate over each article object:
<% @articles.each do |article| %> <h3><a href="/articles/<%= article.id %>"><%= article.title %></a></h3> <div><%= article.content %></div> <hr> <% end %>
There are a number of controller actions that need to work with a specific article including show, edit, update, and destroy. They call the find method to return an article object and assign it to the @article instance variable like so:
@article = Article.find(params[:id])
Notice that the class Article is the receiver of the find method. It seems counterintuitive, but find is a class method not an instance method. That's because when you call it, you don't yet have the object. That's the task of the find method. To return the requested article object.
The find method is called on the model's Article class, but it is defined in the inherited HacktiveRecord::Base class:
def self.find(id) row = DB.execute("SELECT * FROM #{table} WHERE id = ? LIMIT 1", id).first # 1 puts("\s #{self} SQL Statement: SELECT * FROM #{table} WHERE id = #{id} LIMIT 1") # 2 attributes = Hash[columns.zip(row)] # 3 object = self.new(attributes) # 4 return object end
1) The first statement is a database query looking for a row with the id value passed in. We add a limit of 1 so the database stops looking once it finds one row. It returns an array of the row's values for id, title, content, and created_at. It actually returns an array of arrays. The outer array contains one element for each row. We tack on the Ruby first method which returns the first element in the array, which works out great since we only have one element in the outer array.
2) Print the query to our logs with puts.
3) Get a hash of the attributes by zipping the row values to the columns, which become the hash keys.
4-5) Call the new method on self (i.e., the Article class) to instantiate and return a new Article object based on the attributes we got from our database query.
When a user wants to create a new article they click on the new article link. That presents a page with a form to fill out. When they submit the form it sends a POST request with the form data that gets routed to the controller's create action:
def create @article = Article.new(title: params['article']['title'], content: params['article']['content']) @article.save redirect_to "/articles/#{@article.id}" end
There are three distinct things going on in this controller action. The first statement creates a new article object. The second statement saves the object to the database. The third object gets the new article id and uses it to redirect to the new article's show page.
The first statement uses the Ruby new method on the Article class to create a new article object. It sends the title and content values as arguments. The values come from the POST data's params as submitted by the article form. Just as we created new articles from IRB, the Article class will instantiate a new Article object:
class Article < HacktiveRecord::Base attr_accessor(:id, :title, :content, :created_at) def initialize(id: nil, title: nil, content: nil, created_at: timestamp) @id = id @title = title @content = content @created_at = created_at end def timestamp Time.now.to_s end ... end
The new object will have an id of nil, a title and content as submitted in the form, and a created_at date as generated from the timestamp method. We assign this object to the @article instance variable.
The second statement in the articles controller index action saves the @article object, which is currently in our computer's RAM memory as a Ruby object, to our database. It calls the save method on the object so this is an instance method, defined in HacktiveRecord::Base.
def save object = self # 1 columns = object.class.columns # 2 columns.delete(:id) # 3 placeholders = (["?"] * (columns.size)).join(",") # 4 values = columns.map { |key| object.send(key) } # 5 columns = columns.join(",") # 6 DB.execute("INSERT INTO #{object.class.table} (#{columns}) VALUES (#{placeholders})", values) # 7 puts("\s #{self.class} SQL Statement: INSERT INTO #{self.class.table} (#{columns}) VALUES (#{placeholders})" + values.to_s) # 9 object.id = DB.execute("SELECT last_insert_rowid()")[0][0] # 10 return object end
1) The first statement is just a convenience so that it's clear that self represents the new Article object that was assigned to @article in the controller. It is the receiver of the save method.
2) Chain the "class" method to the new article object to get it's class, then call the columns class method to get an array of the database column names.
3) Remove the id column since we don't have an id for our object yet.
4) Return a string of one question mark for each column separated by commas. We will be using a prepared statement for our database insert, which uses ? as a placeholder.
5) Return an array of values that we'll be inserting into the database. We apply the map method to the columns array which applies a block to each element in the columns array and returns a new array of the transformed elements. Each column name should correspond to a key in the article object's attributes. The block takes each column name and applies the send method to the object. Because our model's Article class employs the attr_accessor method, we can treat each attribute name as a getter or setter method. Applying the send method on the object with the column name as the key will return the attribute's value. Thus we get an array of the object's values for each column name.
6) Return a string of the column names joined by commas. We'll use this for the database insert.
7) Execute the SQL INSERT statement on the database. Now a copy of the object is persisted as a row in the database.
8) We could stop here but if we want to do a redirect in the controller to the new article's show page then we need the new article's id. The database generates that for us but doesn't return it when we do an insert. We need to do a separate query. The last_insert_rowid returns the id of the last row created, which we assign to our original object.
9) Log the SQL statement to the terminal using puts. The values variable is an array and puts can only print strings so we have to convert it to a string with values.to_s.
10-11) We return the original object, including the new id assigned to it by the database. The articles controller index action can now do a redirect to the new article's show page using @article.id.
When a user wants to modify an article, they click the edit link which takes them to a page with an edit form populated with the @article values. The article's edit and update controller actions are below:
def edit @article = Article.find(params[:id]) render end def update @article = Article.find(params[:id]) @article.update(title: params['article']['title'], content: params['article']['content']) redirect_to "/articles/#{@article.id}" end
The @article instance variable contains the requested article object. The second statement in the update action applies the update method to the @article object making update an instance method. It passes two key value pairs as arguments for title and content. The values are taken from the submitted edit form. The values are sent from the client in the HTTP request data and accessed as params. The update method is defined in the HacktiveRecord::Base class:
def update(attributes={}) columns = attributes.keys # 1 columns = columns.map { |column| "#{column}=?" }.join(",") # 2 values = attributes.values # 3 values << id # 4 DB.execute("UPDATE #{self.class.table} SET #{columns} WHERE id = ?", values) # 5 puts("\s #{self.class} SQL Statement: UPDATE #{self.class.table} SET #{columns} WHERE id = ?" + values.to_s) # 6 end
The update method takes one argument, attributes, which has a default value of an empty hash. The two key:value pairs sent as arguments are received in the attributes parameter hash.
1) Apply the keys method to the attributes hash giving us an array of the column names.
2) Use the map method to transform the array into a string that looks like: title=?,content=?. This is going to be a prepared statement using ? as placeholders for the values.
3) Get the values by applying the values method to our attributes array.
4) We did not pass in the id as an argument in the update method because we don't want a user to change the id. But our database query will use the id in the where clause. The where clause in our database update also uses a ? placeholder, so we'll tack on the object's id value to the values array. Since this is an instance method, id is taken from the receiver instance @article.
5) Execute the SQL statement to update the database.
6) Log the SQL statement to the terminal with puts.
If a user clicks on the delete link on an article's show page it will be routed to the articles controller destroy action. The first statement uses the find method to create an article object assigned to @article. The second statement chains the destory method to it.
def destroy @article = Article.find(params[:id]) @article.destroy redirect_to("/articles") end
Since the Article object @article is the receiver, destroy is an instance method. It is defined in the HacktiveRecord::Base class:
def destroy DB.execute("DELETE FROM #{self.class.table} WHERE id = ?", id) puts("\s #{self.class} SQL Statement: DELETE FROM #{self.class.table} WHERE id = #{id}") end
This method simply executes a database delete using the object's id attribute. Then logs it to the terminal.
Now restart the server (Control+C to stop, rackup
to restart). Cross your fingers, then refresh your browser. Try out the shiny new RESTful resources to view all articles, view one, create a new article, edit it, then delete it. Sweet.
That concludes the resource, model and database portion of this tutorial. For added practice there's an optional Part b to this section that shows you some other things you can do, like use Active Record as your ORM.
Models and Persistence - Bonus Section
The next section is a short one on our Support module.