CRUD Operations
Overview
This page builds on the concepts discussed in the Backend page.
To follow this section, create a simple Rails application..
The complete config files can be found here: Github/test-rails-app
In the previous page, the application was created and the article has been generated using scaffold. This time, we want to create the resource manually without using scaffold.
Note that article follows a fixed structure that connects different layers of the app:
- Table:
articles - Model:
article(singular, CamelCase)- Model file name:
article.rb - Model class name:
Article
- Model file name:
The article model (singular, CamelCase) is automatically mapped to an articles table (plural).
(Optional) Cleanup Scaffolded Resources
If you generated the articles table, you can remove everything that scaffold created:
-
Roll back the scaffold migration (Recommended)
If you haven’t committed yet, this is the safest way.
rails db:rollbackIf there were multiple migrations run:
rails db:rollback STEP=1This will:
- Drop the
articlestable - Revert the database schema
- Drop the
-
Remove these files/directories if they exist:
app/models/article.rbapp/controllers/articles_controller.rbapp/views/articles/app/helpers/articles_helper.rbapp/assets/stylesheets/articles.scss(or.css)test/models/article_test.rborspec/models/article_spec.rbtest/controllers/articles_controller_test.rbor specs
-
Edit
config/routes.rband delete:resources :articles -
If you want it fully gone:
rm db/migrate/*create_articles*.rbThen reset the database if needed:
rails db:reset⚠️ Warning:
db:resetdrops and recreates the database — only do this in dev.
articles Table Structure
Before creating the table, we define what it needs to store.
-
id- Serves as the primary key
- Auto-generated by Rails
- Uniquely identifies each article
-
title- Uses the
stringtype - Limited in length (255 characters)
- Uses the
-
description- Uses the
texttype - Supports longer content
- Uses the
Here's a sample table:
| id | title | description |
|---|---|---|
| 1 | First post | Introduction article |
| 2 | Second post | Follow-up content |
| 3 | Third post | Another sample article |
| 4 | Fourth post | Deeper topic overview |
| 5 | Fifth post | Final example entry |
Creating the Table
Tables are created through migration files. Migrations describe database changes and are applied in order. In the example below, a migration is generated to create the articles table.
rails generate migration create_articles
Rails creates a new migration file called db/migrate/20230111005920_create_articles.rb. The timestamp in the filename ensures migrations run in the correct order.
class CreateArticles < ActiveRecord::Migration[6.1]
def change
create_table :articles do |t|
t.timestamps
end
end
end
Note: When you use the create_ prefix in the migration name, Rails assumes you are creating a table and auto-generates a create_table block, which includes the t.timestamps.
Modify the articles table and add a title column with a string type:
class CreateArticles < ActiveRecord::Migration[6.1]
def change
create_table :articles do |t|
t.timestamps
t.string :title
end
end
end
Apply the migration to create the table.
rails db:migrate
Rails runs any migrations that have not been applied yet. After this completes, the articles table exists in the database. You can confirm this in db/schema.rb.
Note: I have another application_records in my schema because I have a another migration file in my db folder.
ActiveRecord::Schema[6.1].define(version: 2023_01_11_005920) do
create_table "application_records", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "articles", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "title"
end
end
Once a migration has been run, Rails will not run it again because Rails tracks executed migrations.
- Re-running
rails db:migrateskips completed files - Changes to old migrations are ignored
Editing a previously run migration will not update the database schema. This behavior prevents unexpected changes across environments. To update the table, you would need to do a rollback.
Roll Back a Migration
The most recent migration can be undone using a rollback.
rails db:rollback
Output:
== 20230111005920 CreateArticles: reverting ===================================
-- drop_table(:articles)
-> 0.0085s
== 20230111005920 CreateArticles: reverted (0.0210s) ==========================
This reverts the last migration and removes the articles table. If you check db/schema.rb, you’ll see that the articles table is no longer present.
ActiveRecord::Schema[6.1].define(version: 2023_01_11_005920) do
create_table "application_records", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
At this stage, you can modify the migration to add the required fields and run rails db:migrate again to recreate the table. This is fine for local testing, but it’s not recommended for shared projects.
In those cases, the better approach is to create a new migration instead.
Add Changes using a New Migration
The correct way to update the database is to create a new migration.
First, generate a new migration.
rails generate migration add_desc_to_articles
This creates the db/migrate/20230111012349_add_desc_to_articles.rb. Since the migration name we used is generic, Rails didn't know what to add in the file.
## db/migrate/20230111012349_add_desc_to_articles.rb
class AddDescToArticles < ActiveRecord::Migration[6.1]
def change
end
end
Update the migration file to add the description columns.
## db/migrate/20230111012349_add_desc_to_articles.rb
class AddDescToArticles < ActiveRecord::Migration[6.1]
def change
add_column :articles,
:description,
:text
end
end
Run the migration to apply the changes.
rails db:migrate
Note: Because we rolled back the migration earlier, the articles table had not been created yet. Running the migrate again now creates the table with both the title and description columns.
Checking the db/schema.rb again:
ActiveRecord::Schema[6.1].define(version: 2023_01_11_014617) do
create_table "application_records", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "articles", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "description"
t.string "title"
end
end
With the database ready, the next step is to work with the Article model and connect it to the rest of the application.
Create an Article Model
To work with the articles table, we need a model that connects the application logic to the database and handles data access.
Create the file app/models/article.rb. This file defines the Article model, and it inherits from ApplicationRecord, which is the base class for all models.
## app/models/article.rb
class Article < ApplicationRecord
end
With this in place, Rails provides getters and setters for fields such as title and description, and it allows the application to communicate with the articles table.
Using the Rails console
We can use the Rails console to directly interact with the database through models. It runs inside the application context and is commonly used for testing and debugging
From the project directory, run:
rails console
Once inside the console, you can test the connection to the articles table.
Calling .all fetches all rows:
Article.all
Output:
Article Load (52.6ms) SELECT "articles".* FROM "articles" /* loading for pp */ LIMIT 11 /*application='TestRailsApp'*/
=> []
Here, the Article class represents the articles table. If the result is an empty collection, it means the connection is working but there are no records yet.
There are three ways to create an article from the Rails console:
- Use
create - Use
newfollowed bysave - Use
newwith attributes in one line
1. Using create
You can use the create method on the model class to add a record. This both builds the object and saves it to the database.
In the example below, title and description are column names from the articles table.
Article.create(
title: "First post",
description: "Introduction article"
)
Output:
TRANSACTION (1.6ms) BEGIN immediate TRANSACTION /*application='TestRailsApp'*/
Article Create (4.0ms) INSERT INTO "articles" ("created_at", "title", "updated_at", "description") VALUES ('2023-01-11 02:17:34.505517', 'First post', '2023-01-11 02:17:34.505517', 'Introduction article') RETURNING "id" /*application='TestRailsApp'*/
TRANSACTION (12.8ms) COMMIT TRANSACTION /*application='TestRailsApp'*/
=>
#<Article:0x000076d895952e98
id: 1,
created_at: "2023-01-11 02:17:34.505517000 +0000",
title: "First post",
updated_at: "2023-01-11 02:17:34.505517000 +0000",
description: "Introduction article">
Rails runs SQL statements behind the scenere to insert the row into the database and automatically fills in the ID and timestamps.
You can verify this by running .all again:
$ Article.all
Article Load (3.6ms) SELECT "articles".* FROM "articles" /* loading for pp */ LIMIT 11 /*application='TestRailsApp'*/
=>
[#<Article:0x000076d895addfd8
id: 1,
created_at: "2023-01-11 02:17:34.505517000 +0000",
title: "First post",
updated_at: "2023-01-11 02:17:34.505517000 +0000",
description: "Introduction article">]
2. Using new and save
Another common approach is to create an object first and save it later.
newcreates an in-memory objectsavewrites it to the database
Run the commands below:
article = Article.new
article.title = "Second post"
article.description = "Follow-up content"
article.save
Before calling save, the object exists only in memory. After saving, the ID and timestamps are filled in because the record now exists in the database.Che
Checking the rows again:
$ Article.all
Article Load (2.7ms) SELECT "articles".* FROM "articles" /* loading for pp */ LIMIT 11 /*application='TestRailsApp'*/
=>
[#<Article:0x000076d89703d4a0
id: 1,
created_at: "2023-01-11 02:17:34.505517000 +0000",
title: "First post",
updated_at: "2023-01-11 02:17:34.505517000 +0000",
description: "Introduction article">,
#<Article:0x000076d89703d360
id: 2,
created_at: "2023-01-11 02:21:36.948319000 +0000",
title: "Second post",
updated_at: "2023-01-11 02:21:36.948319000 +0000",
description: "Follow-up content">]
3. Use new with attributes in one line
You can also pass attributes directly when creating the object, then save it afterward.
article = Article.new(
title: "Third post",
description: "Another sample article"
)
article.save
This produces the same result, but uses fewer steps.
To confirm the records were created:
$ Article.all
Article Load (2.5ms) SELECT "articles".* FROM "articles" /* loading for pp */ LIMIT 11 /*application='TestRailsApp'*/
=>
[#<Article:0x000076d89703f160
id: 1,
created_at: "2023-01-11 02:17:34.505517000 +0000",
title: "First post",
updated_at: "2023-01-11 02:17:34.505517000 +0000",
description: "Introduction article">,
#<Article:0x000076d89703f020
id: 2,
created_at: "2023-01-11 02:21:36.948319000 +0000",
title: "Second post",
updated_at: "2023-01-11 02:21:36.948319000 +0000",
description: "Follow-up content">,
#<Article:0x000076d89703eee0
id: 3,
created_at: "2023-01-11 02:26:38.002644000 +0000",
title: "Third post",
updated_at: "2023-01-11 02:26:38.002644000 +0000",
description: "Another sample article">]
You should now see all articles that were saved. This confirms the model, database, and console are working together as expected.
Exit the Rails console
To leave the console and return to the terminal:
exit
Read, Update, and Delete
After creating articles, we can also read, update, and delete them from the Rails console.
Examples:
-
Fetch the second article using the
find:Article.find(2)Output:
#<Article:0x00007691687993a0
id: 2,
created_at: "2023-01-11 02:21:36.948319000 +0000",
title: "Second post",
updated_at: "2023-01-11 05:22:30.483007000 +0000",
description: "Edited description of second article"> -
Use
firstto get the first article andlastto get the last one, and then chain it to the getters method (likedescription):Article.first.description
# => "Edited description of second article"
Article.last.description
# => "Another sample article" -
We can also assign it to a variable:
article = Article.find(2)Now we can access its attributes using getters:
article.title # returns the title
article.description # returns the descriptionOutput:
=> "Second post"
=> "Follow-up content" -
To update the description, assign a new value to the attribute, then save the record:
article.description = "Edited description of second article"
article.saveOutput:
TRANSACTION (1.3ms) BEGIN immediate TRANSACTION /*application='TestRailsApp'*/
Article Update (3.3ms) UPDATE "articles" SET "updated_at" = '2023-01-11 05:22:30.483007', "description" = 'Edited description of second article' WHERE "articles"."id" = 2 /*application='TestRailsApp'*/
TRANSACTION (1.9ms) COMMIT TRANSACTION /*application='TestRailsApp'*/
=> trueFetching the article again shows the updated value:
Article.find(2).description
# => "Edited description of second article" -
To delete an article, first get the object, then call
destroy:article = Article.last
article.destroyThis removes the article from the database immediately without calling
save.Checking all articles confirms it has been removed:
Article.all
Adding Validations
Currently, the Article model allows creating records without a title or description. To maintain data integrity, we can add validations so empty articles cannot be saved.
Some common validations:
- Validate presence to prevent blank fields
- Set minimum and maximum length for better data quality
Presence Validation
To add presence validations, update app/models/article.rb:
## `app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true
validates :description, presence: true
end
Here, the validation make sures that the title and description is present in order for any article to be saved.
Open the Rails console and reload so the changes take effect:
rails console
reload!
Now if we try to save a new article without a title or description, it will return false:
article = Article.new
article.save
Output:
# => false
If we check the errors on the article object, Rails shows clear messages explaining why the record was not saved.
article.errors.full_messages
# => ["Title can't be blank", "Description can't be blank"]
If we try to create an article with a title but without a description, the save still fails.
article = Article.new(title: "Third post")
article.save
# => false
When we provide both a title and a description, the article saves successfully.
article = Article.new(title: "Third post", description: "This is the third post")
article.save
# => true
Checking all records:
$ Article.all
[#<Article:0x000070858909b418
id: 1,
created_at: "2023-01-11 02:17:34.505517000 +0000",
title: "First post",
updated_at: "2023-01-11 02:17:34.505517000 +0000",
description: "Introduction article">,
#<Article:0x000070858909b2d8
id: 2,
created_at: "2023-01-11 02:21:36.948319000 +0000",
title: "Second post",
updated_at: "2023-01-11 05:22:30.483007000 +0000",
description: "Edited description of second article">,
#<Article:0x000070858909b058
id: 5,
created_at: "2023-01-11 07:42:37.365641000 +0000",
title: "Third post",
updated_at: "2023-01-11 07:42:37.365641000 +0000",
description: "This is the third post">]
Length Validation
Next, we can enforce minimum and maximum length constraints on the title and description so that the content is more meaningful.
## `app/models/article.rb
class Article < ApplicationRecord
validates :title,
presence: true,
length: {
minimum: 6,
maximum: 20
}
validates :description,
presence: true,
length: {
minimum: 10,
maximum: 300
}
end
Reload the console again:
reload!
Trying to save an article with a very short title and description fails validation:
article = Article.new(title: "4th", description: "4th")
article.save
# => false
Inspecting the errors shows why it failed:
article.errors.full_messages
# =>
# ["Title is too short (minimum is 6 characters)",
# "Description is too short (minimum is 10 characters)"]
If we provide a valid title but an overly long description, the save still fails:
article.title = "Fourth post"
article.description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.."
article.save
# => false
The error message confirms the length issue:
article.errors.full_messages
# => ["Description is too long (maximum is 300 characters)"]
Once both the title and description meet the validation rules, the article is saved successfully:
article.title = "Fourth post"
article.description = "This is a 4th article that has a proper length"
article.save
# => true
Listing all articles:
$ Article.all
[#<Article:0x00007c6261f2bf58
id: 1,
created_at: "2023-01-11 02:17:34.505517000 +0000",
title: "First post",
updated_at: "2023-01-11 02:17:34.505517000 +0000",
description: "Introduction article">,
#<Article:0x00007c6261f2be18
id: 2,
created_at: "2023-01-11 02:21:36.948319000 +0000",
title: "Second post",
updated_at: "2023-01-11 05:22:30.483007000 +0000",
description: "Edited description of second article">,
#<Article:0x00007c6261f2bcd8
id: 8,
created_at: "2023-01-11 08:01:45.875675000 +0000",
title: "Third post",
updated_at: "2023-01-11 08:01:45.875675000 +0000",
description: "This is the third post">,
#<Article:0x00007c6261f2bb98
id: 9,
created_at: "2023-01-11 08:02:52.917372000 +0000",
title: "Fourth post",
updated_at: "2023-01-11 08:02:52.917372000 +0000",
description: "This is a 4th article that has a proper length">]
Now our model ensures that only meaningful articles with proper titles and descriptions are saved.
For more information, you can read about Active Record Validations.