Home [Ruby on Rails 8][Tutorial] DB 의존관계 맺기 기초
Post
Cancel

[Ruby on Rails 8][Tutorial] DB 의존관계 맺기 기초

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

개요

제품에 재고가 채워지면 email 로 알림을 받을 수 있도록 만들것이다.
제품당 여러명에게 email 을 보내야하므로 products 테이블에 email 을 추가하면 안되고 새로운 table 을 만들어서 email 을 등록할 것이다.
user 테이블에 email 을 추가 후 관계를 맺어도 되지만, 여기서는 구독자 table 을 만들어서 관계를 만들것이다.

model 생성

1
2
3
4
5
6
7
$ bin/rails generate model Subscriber product:belongs_to email
      invoke  active_record
      create    db/migrate/20251103133732_create_subscribers.rb
      create    app/models/subscriber.rb
      invoke    test_unit
      create      test/models/subscriber_test.rb
      create      test/fixtures/subscribers.yml

susbcribers 에 products_id 를 외래키로 넣기위해 product:belongs_to 를 작성해준다.
그리고 email column 이 있어야하므로 email 도 넣었다.

이렇게 생성된 테이블은 아래와 같다.

|id|product_id|email|created_at|updated_at| |—|—|—|—|—|

create_subscribers.rb 파일을 열어보자.

1
2
3
4
5
6
7
8
9
10
class CreateSubscribers < ActiveRecord::Migration[8.0]
  def change
    create_table :subscribers do |t|
      t.belongs_to :product, null: false, foreign_key: true
      t.string :email

      t.timestamps
    end
  end
end

자동으로 코드를 작성해준 모습을 볼 수 있다.
반대로 생각하면 t.belongs_to :product, null: false, foreign_key: true 부분이 products_id 를 외래키로 추가하는 코드인 것을 알 수 있다.

$ bin/rails db:migrate를 입력하며 반영하자.

여기까지만 하면 아쉽게도 의존관계가 완성되지 않는다.
subscribers 가 product_id 를 외래키로 사용하지만 products 는 모르는 상태(?) 라고 생각하면 쉽다.
그래서 products 에서도 subscribers 에 대한 의존관계를 설정해야한다.

1
2
3
4
5
6
7
8
class Product < ApplicationRecord
  has_many :subscribers, dependent: :destroy
  has_one_attached :featured_image
  has_rich_text :description

  validates :name, presence: true
  validates :inventory_count, numericality: { greater_than_or_equal_to: 0}
end

Product model 에 has_many :subscribers 를 추가하여 1:다 의존관계를 설정한다.
product 하나당 여러개의 email 이 존재할 수 있기 때문이다.
만약 1:1 관계로 설정하려면 has_one 으로 작성하면 된다.
dependent 에 :destroy 를 주면 데이터 삭제시 의존하는 데이터도 같이 삭제된다.
dependent 에 :nullify 를 주면 데이터 삭제시 의존하는 컬럼에 null 값이 들어간다.

액션 작성

구독 버튼을 클릭하면 table 에 email 을 저장하고 client 에게 구독되었다는 메세지를 전달해보자.
구독 버튼은 뒤에서 구현하고 controller 부터 구현하자.

form 의 구독버튼을 클릭하면 record 를 추가할 것이므로 create 액션이 적합하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SubscribersController < ApplicationController
    allow_unauthenticated_access
    before_action :set_product

    def create
        @product.subscribers.where(subscriber_params).first_or_create
        redirect_to @product, notice: "You are now subscribed."
    end

    private
        def set_product
            @product = Product.find(params[:product_id])
        end

        def subscriber_params
            params.expect(subscriber: [:email])
        end
end

create 에 작성된 코드를 차근차근 알아보자

1
@product.subscribers.where(subscriber_params).first_or_create

앞서 Product model 에 subscribers 테이블과 연관관계를 맺었다.
그래서 @product.subscribers 를 사용해 subscribers 테이블의 records 를 가져올 수 있다.
다음 쿼리를 수행하게 된다.

1
2
3
select "subscribers".* 
from "subscribers" 
where "subscribers"."product_id" = @product.id

거기에 where 조건문을 사용하여 다음 쿼리문이 되었다.

1
2
3
4
select "subscribers".*
from "subscribers"
where "subscribers"."product_id" = @product.id
and "subscribers"."email" = 사용자가 입력한 값

.first_or_create 는 조회된 첫번째 record 를 리턴하거나 새로 만들라는 의미이다.
조건을 만족하는 record 가 없으므로 조건을 만족하는 record 를 새로 만들게된다.

1
redirect_to @product, notice: "You are now subscribed."

redrect_to @product 는 설명을 생략한다.
notice 는 데이터를 임시로 보관하는 flash 이다.
notice 라는 flash 에 “you are now subscribed” 문자열을 보관한 것이다.
flash 는 다음 액션 전까지 사용가능한 데이터 임시 보관장소이다.

1
2
3
4
5
6
7
8
9
<%# app/views/layouts/application.html.erb %>
<html>
  <%# 생략 %>
  <body>
    <div class="notice"><%= flash[:notice] %></div>
    <div class="alert"><%= flash[:alert] %></div>
    <%# 생략 %>
  </body>
</html>

flash 에 저장된 값을 꺼내려면 위 코드처럼 falsh[:flash명] 으로 작성하면 된다.

사진1

이미지로 보면 이해가 쉽다.

라우팅

subscribers#create 는 만들었지만 해당 액션으로 가는 라우팅을 설정하지 않았다. 설정해주자.

1
  resources :subscribers, only: [ :create ]

이러면 path 는 POST /subscribers 가 될 것이다.
하지만 우리가 원하는건 특정 product 에 대해 subscribers 를 설정하는 것이다.
POST /products/:id/subscribers 가 되야한다.

1
2
3
  resources :products do
    resources :subscribers, only: [ :create ]
  end

nested(중첩) route 를 사용했다.
이제 POST /products/:id/subscribers 가 subscribers#create 를 호출하게 된다.

나는 여기서 의문이 있었다. 중첩되는것까진 이해가 되는데 products/subscribers 가 아니라 products/:id/subscribers 인 이유가 무엇일까?
그 해답은 nested route 가 다음내용을 전제하고 있기 때문이다.
nested route 는 리소스가 다른 리소스를 가질 수 있을 때 사용한다.
예를 들어 products 의 product 가 subscribers 를 가질 수 있을 때 사용하면된다.
즉, 각각의 product 가 subscribers 를 가지고 있을 때 nested route 를 사용하는 것이므로 반드시 :id 가 필요한 것이다.

form 작성

1
2
3
4
5
6
7
8
9
10
11
12
<%# app/views/products/_inventory.html.erb %>
<% if product.inventory_count.positive? %>
  <p><%= product.inventory_count %> in stock</p>
<% else %>
  <p>Out of stock</p>
  <p>Email me when available.</p>

  <%= form_with model: [product, Subscriber.new] do |form| %>
    <%= form.email_field :email, placeholder: "you@example.com", required: true %>
    <%= form.submit "Submit" %>
  <% end %>
<% end %>

_inventory.html.erb 파일을 생성하고 구독 이메일을 입력하는 form 을 만든다.

1
2
3
4
5
<%# app/view/products/show.html.erb %>
생략
<%= render "inventory", product: @product %>
생략
<% end %>

해당 form 을 제품 상세 페이지에서 렌더링하도록 한다.
사진2

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

[Ruby on Rails 8][Tutorial] 테이블에 column 추가

[Ruby on Rails 8][Tutorial] action mailer 와 email 알림