Model
Model is the first component of MVC. It's the core of data management in Rails because it lives between the controller and the database. This is done by Object-relational Mapping we discussed earlier. It's an abstraction designed in a way such that you don't have to directly interact with the database using SQL.
Model is a generic name for any MVC frameworks. For Rails, it's been given a special name ActiveRecord. So, in the future, when you are dealing with ActiveRecord in Rails, you are working with the Model part of MVC.
ActiveRecord Models and Database Tables are coupled together. When you create a table called books
in the database, a model called Book
will be generated and linked to the database model.
Let's try to implement our previous relational database example with books and authors in ActiveRecord in Rails.
So, in order to store data about books, let's create both the model called Book
and the actual table books
in our database.
In Rails, you can run $ rails generate model <name in singular form>
to generate the necessary files to get started.
$ rails g model book
Running via Spring preloader in process 17589
invoke active_record
create db/migrate/20161212100533_create_books.rb
create app/models/book.rb
invoke test_unit
create test/models/book_test.rb
create test/fixtures/books.yml
Notice that
book
is singular because all model names are singular in rails
Here, db/migrate/xxxx_create_books.rb
is the migration file necessary to modify the database schema. app/models/book.rb
is the model file for the MVC framework.
In db/migrate/xxxx_create_books.rb
,
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :books do |t|
t.timestamps
end
end
end
Let's look at this file:
- We are creating a new class called
CreateBooks
which will inheritActiveRecord::Migration
. We don't know what's in the superclassActiveRecord::Migration
, but that's fine for now. We just need to know the conventions of using it. That's the point of using Rails. - The instance method
change
is for changing and modifying the database schema. - Inside the
change
method, we have another method calledcreate_table
with the parameter:books
as the table name. This means that we are trying to create a table calledbooks
in the database. - Also, what is
t.timestamps
? This will create the timestamp forcreated_at
andupdated_at
to track the time when a record is created or updated.
Inside create_table
, we can configure how we like the table to be. For example, we want the table books
to have the following attributes: name
, publishing_year
and num_of_pages
(number of pages).
So, how do we modify this file to add the attributes name
, publishing_year
and num_of_pages
?
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :books do |t|
# t.<data type> :<attribute name>
t.string :name
t.string :publishing_year
t.integer :num_of_pages
t.timestamps
end
end
end
t.string :name
defines that we want an attribute calledname
with the data type stringt.string :publishing_year
defines an attribute calledpublishing_year
with the data type stringt.integer :num_of_pages
defines an attribute callednum_of_pages
with the data type integer (number)
Now, we have finished defining how we want to modify the database schema. But, have we actually modified the schema yet? No. We were only defining the changes. If we want it to take effect, we have to run another command to execute the changes.
In order to modify the database schema, we need to not only define but also "migrate" migration files.
$ rails db:migrate
== 20161212101030 CreateBooks: migrating ======================================
-- create_table(:books)
-> 0.0020s
== 20161212101030 CreateBooks: migrated (0.0022s) =============================
This log message tells you that your migration file has successfully modified the database schema.
You can check the migration file status with $ rails db:migrate:status
Status Migration ID Migration Name
--------------------------------------------------
up 20161212101030 Create books
Great! We have successfully created a table in our database, just like how we created a new spreadsheet in Excel. We are ready to put in some data. In Excel, it would be easy. I just click and add some rows of data. But, how do you do it in Rails?
Well, remember MVC's Model is automatically linked to the database? Let's look at the other file that we've generated app/models/book.rb
.
In app/models/book.rb
,
class Book < ApplicationRecord
end
We have a Book
class inheriting the superclass ApplicationRecord
. We've defined nothing in Book
yet. But, Book
has already linked with the books
table in the database.
Let's try to use the model Book
to create a new record for the books
table in the database. We first need to go into Rails console.
Why do we need Rails console? Because we want to run code in Rail's environment.
$ rails console
Loading development environment (Rails 5.0.0.1)
irb(main)>
To exit Rails console, simply type "exit" and hit enter
Now, let's try to insert a new record on the books table through the model Book
with Book.create
.
irb(main)> Book.create
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "books" ("created_at", "updated_at") VALUES (?, ?) [["created_at", 2016-12-13 01:51:52 UTC], ["updated_at", 2016-12-13 01:51:52 UTC]]
(1.0ms) commit transaction
=> #<Book id: 1, name: nil, publishing_year: nil, num_of_pages: nil, created_at: "2016-12-13 01:51:52", updated_at: "2016-12-13 01:51:52">
Let's look at the log here.
begin transaction
signals that it has initiated the process with the database.- A "transaction" occurs every time you create / update / delete record from the database.
SQL (0.6ms) INSERT INTO "books" ("created_at", "updated_at") VALUES (?, ?) [["created_at", 2016-12-13 01:51:52 UTC], ["updated_at", 2016-12-13 01:51:52 UTC]]
is SQL that was generated for you when you useBook.create
, so you don't need to type it yourself. This is where the ORM magic happens. Remember we said this earlier:We use SQL to add, update, retrieve and delete records. There are simple and advanced operations in which you can do. Now, a lot of MVC frameworks recognize that SQL is not a friendly language to use in general practice. You should be able to achieve the same SQL tasks by using, say, Ruby or JavaScript.
commit transaction
is important because the keyword "commit" signals that a transaction is successful. On the contrary, "rollback" signals a transaction has failed, hence rolling back to the previous state.#<Book id: 1, name: nil, publishing_year: nil, num_of_pages: nil, created_at: "2016-12-13 01:51:52", updated_at: "2016-12-13 01:51:52">
is the new database record represented as a Ruby object.
Notice that, name
, publishing_year
and num_of_pages
are all nil? That's because we didn't pass those attributes when we created the record. So, let's try to put these attributes in this time creating a new record.
irb(main)> Book.create(name: "Harry Potter and the Philosopher's Stone", publishing_year: "1997", num_of_pages: 223)
...
(0.7ms) commit transaction
=> #<Book id: 2, name: "Harry Potter and the Philosopher's Stone", publishing_year: "1997", num_of_pages: 223, created_at: "2016-12-13 02:05:10", updated_at: "2016-12-13 02:05:10">
Now, we have 2 records in the books table. How do we check it?
irb(main)> Book.count
(0.2ms) SELECT COUNT(*) FROM "books"
=> 2
So, Book.count
returned 2
. The .count
is a class method inherited from ApplicationRecord
. Its job is to generate the raw SQL SELECT COUNT(*) FROM "books"
, send this SQL to the database, and return the number of records in the books table.
How do you access the first record we made and update it? Use Book.find(<id of the record>)
.
irb(main)> Book.find(1)
Book Load (0.3ms) SELECT "books".* FROM "books" WHERE "books"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Book id: 1, name: nil, publishing_year: nil, num_of_pages: nil, created_at: "2016-12-13 01:51:52", updated_at: "2016-12-13 01:51:52">
The ID of the first record starts at 1.
How do you show all records we have created? Book.all
.
irb(main)> Book.all
Book Load (0.2ms) SELECT "books".* FROM "books"
=> #<ActiveRecord::Relation [
#<Book id: 1, name: nil, publishing_year: nil, num_of_pages: nil, created_at: "2016-12-13 01:51:52", updated_at: "2016-12-13 01:51:52">,
#<Book id: 2, name: "Harry Potter and the Philosopher's Stone", publishing_year: "1997", num_of_pages: 223, created_at: "2016-12-13 02:05:10", updated_at: "2016-12-13 02:05:10">
]>
Now you see that Rails has made it super easy for us to interact with the database because it generates all the raw SQL queries for us. The "Rails way of doing things" is also more human friendly and pleasant to use.
So, let's edit our Book
model file book.rb
to add some data validations. For example, let's make sure a book can only be created when the name of the book is present.
In app/models/book.rb
,
class Book < ApplicationRecord
validates :name, presence: true
end
Here, we are validating the presence of the attribute name. It means that name cannot be empty or nil.
We need to exit and re-enter Rails console for our changes to take effect. To exit Rails console: ctrl + c. To get back into Rails console $ rails c
.
irb(main)> Book.create
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Book id: nil, name: nil, publishing_year: nil, num_of_pages: nil, created_at: nil, updated_at: nil>
Now, we see that Book.create
with no attributes didn't work as its transaction has been rollbacked because the attribute name
is not present.
irb(main)> Book.create(name: 'test')
...
(0.8ms) commit transaction
=> #<Book id: 4, name: "test", publishing_year: nil, num_of_pages: nil, created_at: "2016-12-13 03:23:32", updated_at: "2016-12-13 03:23:32">
irb(main):003:0>
This time, transaction goes through when the attribute name
is present.