Home [Ruby on Rails 8][Tutorial] DB 기초
Post
Cancel

[Ruby on Rails 8][Tutorial] DB 기초

Getting Started with Rails
위 튜토리얼을 따라 학습하며 작성한 글입니다.

내장 DB

1
rails new [프로젝트명]

위 명령어로 프로젝트를 생성할 때 DB 를 지정해주지 않으면 자동으로 내장 SQLite 로 지정해준다.

Database model 만들기

1
2
3
4
5
6
7
8
$ bin/rails generate model Product name:string
아래는 출력 결과
      invoke  active_record
      create    db/migrate/20251012122812_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/models/product_test.rb
      create      test/fixtures/products.yml
  • generate model Product name:string
    ‘model 생성해라 이름은 Product 로 하고 name 이라는 column 이 있다’는 의미.
    위 명령어는 아래 작업을 수행한다.

  • invoke active_record
    active_record 에 관련된 작업 호출
    active_record : 관계형 데이터베이스를 Ruby 코드에 매핑하는 Rails의 기능. ORM 이라고 보면되고 MVC 의 Model 에 해당된다.

  • create db/migrate/20251012122812_create_products.rb
    db/migrate 에 있는 파일은 table 을 migration 하는 코드이다.
    여기서는 product 를 create 하는 파일을 생성한다.

  • create app/models/product.rb
    active record 모델 생성한다.

  • create test/fixtures/products.yml
    Fixtures are a way of organizing data that you want to test against; in short, sample data.
    Fixtures는 테스트에 사용할 데이터를 정리하는 방법이며, 간단히 말해 샘플 데이터입니다.
    출처: https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html

Migration

A migration is a set of changes we want to make to our database.
마이그레이션은 데이터베이스에 적용하려는 일련의 변경 사항입니다.

마이그레이션을 수행하기 전 db/migrate/20251012122812_create_products.rb 파일을 열어 무슨 내용이 있는지 확인한다.

1
2
3
4
5
6
7
8
9
class CreateProducts < ActiveRecord::Migration[8.0]
  def change
    create_table :products do |t|
      t.string :name

      t.timestamps
    end
  end
end
namecreated_atupdated_at
   

코드를 살펴보면 위와 같은 테이블을 생성한다는 내용이 있다.
테이블명은 products 인 것을 볼 수 있다.

In contrast to the model above, Rails makes the database table names plural, because the database holds all of the instances of each model (i.e., You are creating a database of products).

rails 는 table name 을 복수형으로 만든다. 왜냐하면 데이터베이스는 각 model 의 instance 들을 모두 가지고 있기 때문이다.
product 들을 가진 table 이므로 table name 을 products 로 한다는 의미같다.

다른 예시로 User 테이블의 각 record 는 User 이므로 테이블 이름은 Users 로 하는게 맞지않냐? 라는 의미로 보인다.

  • t.string :name
    name column 이고 type 은 string 이다.

  • t.timestamps
    created_at 과 updated_at 를 한번에 정의하는 shortcut 이다.

1
$ bin/rails db:migrate

bin/rails db:migrate 를 수행하면 마이그레이션 파일이 수행되어 table 이 만들어지게 된다.

Migration 취소

1
$ bin/rails db:rollback

마지막으로 수행한 마이그레이션을 취소하려면 bin/rails db:rollback 를 입력한다.

Model 확인

1
2
3
4
$ cat app/models/product.rb

class Product < ApplicationRecord
end

bin/rails generate model Product name:string 수행하면서 product.rb 라는 파일이 만들어졌었다.
그리고 열어보면… 충격! Product 클래스를 만들고 끝? 인 것처럼 보인다. 변수도, 메서드도 없다..
뭐 당연히 그럴리 없고 ApplicationRecord 로 뭔가 주입되는 것을 확인할 수 있다.

rails 콘솔에서 Product 클래스를 확인해보자.

1
2
3
4
$ rails console

console> Product.column_names
=> ["id", "name", "created_at", "updated_at"]

오잉 Product 라는 클래스를 인식하고 column_names 라는 메서드도 사용할 수 있다!
아까 위에서 마이그레이션으로 적용시켰던 column들을 확인할 수 있고, ID 도 추가로 있는 모습이다.

결론적으로 products 라는 테이블이 있고, Product 라는 Model 이 class 로 구현된 모습을 확인할 수 있다.
여기까지 확인된 내용으로 그림을 그려보면 대충 다음과 같다.

사진1

Product 클래스와 products 테이블이 있고, 대충 어떻게든 연동되는 모습을 표현했다.
실제로 이렇다는게 아니라 이해를 위한 그림이다.

Record 생성

products 테이블에 record 를 채워넣는 방법을 알아보자.

1
2
console> product_t_shirt = Product.new(name: "T-Shirt")
=> #<Product:0x000000012279b0d0 id: nil, name: "T-Shirt", created_at: nil, updated_at: nil>

Product 클래스로부터 객체를 하나 만들었다.
id, created_at, updated_at 변수는 null 값인걸 확인할 수 있다.

이 객체를 Products 테이블에 record 로 삽입해보자.

1
2
3
4
5
console> product_t_shirt.save
  TRANSACTION (0.2ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Create (3.2ms)  INSERT INTO "products" ("name", "created_at", "updated_at") VALUES ('T-Shirt', '2025-10-12 14:12:29.644957', '2025-10-12 14:12:29.644957') RETURNING "id" /*application='Store'*/
  TRANSACTION (0.5ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

객체.save 를 하면 연동된(?) Database 에 record 로 삽입된다.
출력을 보면 DML 문인 INSERT INTO “products” (“name”, “created_at”, “updated_at”) VALUES (‘T-Shirt’, ‘2025-10-12 14:12:29’, ‘2025-10-12 14:12:29’) 가 실행된 모습이다.

1
2
3
4
5
6
7
console> product_t_shirt
=>
#<Product:0x000000012279b0d0
 id: 1,
 name: "T-Shirt",
 created_at: "2025-10-12 14:12:29.644957000 +0000",
 updated_at: "2025-10-12 14:12:29.644957000 +0000">

객체를 다시 확인해보면 내용이 삽입되어 있다.
근데 객체에 데이터를 왜(why) 자동으로 삽입을 해주는지 잘 모르겠다. 필요하면 개발자가 DB 로 조회후 가져온 데이터로 객체를 채우는게 낫지않나?

1
Product.create(name: "Pants")

객체를 만들지 않고 record 를 바로 삽입하려면 create 를 사용하면 된다.

Select

1
2
3
4
5
6
7
8
9
10
11
12
13
console> Product.all
  Product Load (0.2ms)  SELECT "products".* FROM "products" /* loading for pp */ LIMIT 11 /*application='Store'*/
=>
[#<Product:0x0000000120d3a510
  id: 1,
  name: "T-Shirt",
  created_at: "2025-10-12 14:12:29.644957000 +0000",
  updated_at: "2025-10-12 14:12:29.644957000 +0000">,
 #<Product:0x0000000120d3a3d0
  id: 2,
  name: "Pants",
  created_at: "2025-10-12 14:24:11.713671000 +0000",
  updated_at: "2025-10-12 14:24:11.713671000 +0000">]

Product 클래스와 연결된 products 테이블에 select * 쿼리를 날린다.
SELECT “products”.*
FROM “products” 쿼리문이 수행되는 모습을 확인할 수 있다.

Where

1
2
3
console> Product.where(name: "Pants")
  Product Load (0.2ms)  SELECT "products".* FROM "products" WHERE "products"."name" = 'Pants' /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000120c94d90 id: 2, name: "Pants", created_at: "2025-10-12 14:24:11.713671000 +0000", updated_at: "2025-10-12 14:24:11.713671000 +0000">]

SELECT “products”.*
FROM “products”
WHERE “products”.”name” = ‘Pants’ 쿼리를 수행하는 모습을 확인할 수 있다.

Order

1
2
3
4
5
console> Product.order(name: :asc)
  Product Load (2.5ms)  SELECT "products".* FROM "products" /* loading for pp */ ORDER BY "products"."name" ASC LIMIT 11 /*application='Store'*/
=>
[#<Product:0x000000011e9ce108 id: 2, name: "Pants", created_at: "2025-10-12 14:24:11.713671000 +0000", updated_at: "2025-10-12 14:24:11.713671000 +0000">,
 #<Product:0x000000011e9cdfc8 id: 1, name: "T-Shirt", created_at: "2025-10-12 14:12:29.644957000 +0000", updated_at: "2025-10-12 14:12:29.644957000 +0000">]

SELECT “products”.*
FROM “products”
ORDER BY “products”.”name” ASC 쿼리문이 수행되는 것을 확인할 수 있다.

find by id

1
2
3
console> Product.find(1)
  Product Load (0.4ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> #<Product:0x0000000122255148 id: 1, name: "T-Shirt", created_at: "2025-10-12 14:12:29.644957000 +0000", updated_at: "2025-10-12 14:12:29.644957000 +0000">

id 를 기반으로 record 하나만 검색할 때는 find 를 사용한다.
쿼리문에 LIMIT 1 이 걸려있는걸 보면 반드시 하나만 반환하는 것을 알 수 있다.

1
2
3
console> Product.where(id: "1")
  Product Load (2.1ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 1 /* loading for pp */ LIMIT 11 /*application='Store'*/
=> [#<Product:0x0000000120d36a50 id: 1, name: "T-Shirt", created_at: "2025-10-12 14:12:29.644957000 +0000", updated_at: "2025-10-12 14:12:29.644957000 +0000">]

Product.where(id: “1”) 과 비교하면 차이를 알 수 있다.
where 메서드를 사용하면 반환값이 ActiveRecord::Relation 이지만 find(1) 을 사용하면 product 인스턴스 그 자체가 반환된다.
그래서 번거롭게 따로 추출할 필요 없이, 아래 update 예시처럼 사용할 수 있다.

Update

1
2
3
4
5
6
7
console> find_by_id = Product.find(1)

console> find_by_id.update(name: "Shoes")
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Update (1.1ms)  UPDATE "products" SET "name" = 'Shoes', "updated_at" = '2025-10-12 14:43:52.579845' WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (1.1ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

record 를 update 하는 방법은 2 종류가 있다.

  1. update
  2. save

update 방법의 예시는 바로 위에 있고, save 방법은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
console> find_by_id = Product.find(1)
  Product Load (0.5ms)  SELECT "products".* FROM "products" WHERE "products"."id" = 1 LIMIT 1 /*application='Store'*/
=> #<Product:0x0000000122250a08 id: 1, name: "Shoes", created_at: "2025-10-12 14:12:29.644957000 +0000", updated_at: "2025-10-12 14:43:52.579845000 +0000">

console> find_by_id.name="T-Shirt"
=> "T-Shirt"

console>  find_by_id.save
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Store'*/
  Product Update (1.5ms)  UPDATE "products" SET "name" = 'T-Shirt', "updated_at" = '2025-10-12 14:48:19.154681' WHERE "products"."id" = 1 /*application='Store'*/
  TRANSACTION (0.4ms)  COMMIT TRANSACTION /*application='Store'*/
=> true

객체의 속성 값을 할당하고 save 하면 된다. 사실 제일 처음 Record 생성에서 배웠던 방법과 동일하다.

Delete

1
2
3
4
5
console> product = Product.find(1)

console> product.destroy

console> Product.all

record 를 삭제하려면 record 를 가져와서 destroy 를 수행하면 된다.
수행 후 all 로 확인해보면 삭제된 것을 볼 수 있다.

Validations

제약조건을 설명하기 전에 우선 ruby 문법을 하나 알아야 이해하기 쉽다.
ruby 는 class 가 정의될 때(아니면 수행될 때?) 수행할 메서드를 작성할 수 있다.
java 의 정적 초기화 블록이랑 유사하다.

1
2
3
console> class MyClass
console>   puts "이게 출력 된다고?"
console> end

위 코드를 입력하면 puts 메서드가 수행된다.
객체를 생성할 때 수행되는 생성자도 아니고, 특정 메서드 안에서 수행되는 것도 아니다. 클래스 생성 시점에 수행된다.
이제 validations 를 설정하는 방법을 배워보자.

1
2
3
4
5
6
console> exit
$ vi app/models/product.rb

class Product < ApplicationRecord
  validates :name, presence: true
end

내부에 validates :name, presence: true 한 줄 추가했다.
타 언어로 비유하면 validates(:name, { presence: true }) 라고 생각하면 된다.

validates 메서드는 제약조건을 설정해준다.
위 제약조건은 name 속성이 presence(존재) = true 이어야 된다는 의미이고, nullable = false 와 같다.
콘솔로 가서 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console> reload!
console> product = Product.new
console> product.save
=> false

console> product.errors
=> #<ActiveModel::Errors 
      [#<ActiveModel::Error 
          attribute=name, type=blank, options={}
        >
      ]
    >

console> product.errors.full_messages
=> ["Name can't be blank"]

reload! 는 새로고침이다. 만약 console 을 나갔다 들어온게 아니라 문서 편집기를 사용했다면 반영이 안되어 있을 수 있다. 새로고침 한다.
product.save 로 저장을 시도했지만 실패했다. name 속성이 없기때문이다.
오류 list 를 보려면 errors 메서드를 호출한다.
ActiveModel::Errors 객체가 하나 반환되었고 그 안에 ActiveModel::Error 가 하나 있는것을 볼 수 있다.
아직 두 객체의 사용법은 안배우고 그런게 있구나 하고 넘어간다.
정확한 에러 메시지를 보려면 errors.full_messages 를 호출해 볼 수 있다.

This post is licensed under CC BY 4.0 by the author.

[vue3] pinia actions

[Ruby on Rails 8][Tutorial] Route