1 Composite Primary Keys란 무엇인가?
때로는 테이블의 각 행을 고유하게 식별하기 위해 단일 컬럼의 값으로는 충분하지 않고, 두 개 이상의 컬럼 조합이 필요할 수 있습니다.
이는 primary key로 단일 id
컬럼이 없는 레거시 데이터베이스 스키마를 사용하거나, sharding이나 multitenancy를 위해 스키마를 변경할 때 발생할 수 있습니다.
Composite primary key는 복잡성을 증가시키고 단일 primary key 컬럼보다 느릴 수 있습니다. Composite primary key를 사용하기 전에 해당 사용 사례에서 정말 필요한지 확인하세요.
2 Composite Primary Key Migration
배열 값과 함께 create_table
에 :primary_key
옵션을 전달하여 composite primary key가 있는 테이블을 생성할 수 있습니다:
class CreateProducts < ActiveRecord::Migration[8.1]
def change
create_table :products, primary_key: [:store_id, :sku] do |t|
t.integer :store_id
t.string :sku
t.text :description
end
end
end
3 Querying Models
4 모델 쿼리하기
4.1 #find
사용하기
만약 테이블이 복합 primary key를 사용한다면, #find
로 레코드를 찾을 때 배열을 전달해야 합니다:
# store_id가 3이고 sku가 "XYZ12345"인 product를 찾습니다
irb> product = Product.find([3, "XYZ12345"])
=> #<Product store_id: 3, sku: "XYZ12345", description: "Yellow socks">
위 내용의 SQL 동등 표현은 다음과 같습니다:
SELECT * FROM products WHERE store_id = 3 AND sku = "XYZ12345"
복합 ID를 사용하여 여러 레코드를 찾으려면, #find
에 배열의 배열을 전달하세요:
# 기본 키가 [1, "ABC98765"]와 [7, "ZZZ11111"]인 products 찾기
irb> products = Product.find([[1, "ABC98765"], [7, "ZZZ11111"]])
=> [
#<Product store_id: 1, sku: "ABC98765", description: "Red Hat">,
#<Product store_id: 7, sku: "ZZZ11111", description: "Green Pants">
]
위의 SQL 대응문은 다음과 같습니다:
SELECT * FROM products WHERE (store_id = 1 AND sku = 'ABC98765' OR store_id = 7 AND sku = 'ZZZ11111')
복합 primary key를 가진 Models는 정렬할 때도 전체 복합 primary key를 사용합니다:
irb> product = Product.first
=> #<Product store_id: 1, sku: "ABC98765", description: "빨간 모자">
위의 SQL 동등한 표현은 다음과 같습니다:
SELECT * FROM products ORDER BY products.store_id ASC, products.sku ASC LIMIT 1
4.2 #where
사용하기
#where
의 Hash 조건들은 tuple과 유사한 문법으로 지정할 수 있습니다.
이는 composite primary key 관계를 쿼리할 때 유용할 수 있습니다:
Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]])
4.2.1 :id
를 사용한 조건
find_by
와 where
같은 메서드에서 조건을 지정할 때, id
를 사용하면 모델의 :id
속성과 매칭됩니다. 이는 전달된 ID가 primary key 값이어야 하는 find
와는 다릅니다.
:id
가 primary key가 아닌 모델(예: composite primary key 모델)에서 find_by(id:)
를 사용할 때는 주의가 필요합니다. 자세한 내용은 Active Record Querying 가이드를 참고하세요.
5 Composite Primary Keys를 가진 모델 간의 Association
Rails는 일반적으로 연관된 모델 간의 primary key-foreign key 관계를 추론할 수 있습니다. 하지만 composite primary key를 다룰 때, Rails는 명시적으로 지정하지 않는 한 기본적으로 composite key의 일부(주로 id
컬럼)만 사용합니다. 이러한 기본 동작은 모델의 composite primary key에 :id
컬럼이 포함되어 있고, 그리고 해당 컬럼이 모든 레코드에서 고유한 경우에만 작동합니다.
다음 예시를 살펴보세요:
class Order < ApplicationRecord
self.primary_key = [:shop_id, :id]
has_many :books
end
class Book < ApplicationRecord
belongs_to :order
end
복합 primary key도 지원됩니다. 단, 이것은 복합 foreign key와 함께 작동하지 않으며, 복합 foreign key는 현재 Active Record에서 지원되지 않습니다.
이 설정에서 Order
는 [:shop_id, :id]
로 구성된 복합 primary key를 가지고 있으며, Book
은 Order
에 belongs to 관계입니다. Rails는 order와 book 간의 관계에서 :id
컬럼이 primary key로 사용되어야 한다고 가정합니다. 그리고 books 테이블의 foreign key 컬럼이 :order_id
라고 추론합니다.
아래에서 우리는 Order
와 그것과 연관된 Book
을 생성합니다:
order = Order.create!(id: [1, 2], status: "pending")
book = order.books.create!(title: "A Cool Book")
책의 order에 접근하기 위해서는 association을 다시 로드합니다:
book.reload.order
book을 데이터베이스에서 다시 로드하고 order를 가져옵니다.
이렇게 하면 Rails는 order에 접근하기 위해 다음과 같은 SQL을 생성할 것입니다:
SELECT * FROM orders WHERE id = 2
Rails가 조회 시 주문의 shop_id
와 id
모두가 아닌 id
만 사용하는 것을 볼 수 있습니다. 이 경우에는 모델의 복합 기본 키가 실제로 :id
컬럼을 포함하고 있고, 또한 이 컬럼이 모든 레코드에 대해 유일하기 때문에 id
만으로도 충분합니다.
하지만 위의 요구사항이 충족되지 않거나 association에서 전체 복합 기본 키를 사용하고 싶은 경우, association에 foreign_key:
옵션을 설정할 수 있습니다. 이 옵션은 association에 복합 외래 키를 지정합니다. 관련 레코드를 조회할 때는 외래 키의 모든 컬럼이 사용됩니다. 예를 들어:
class Author < ApplicationRecord
self.primary_key = [:first_name, :last_name]
has_many :books, foreign_key: [:first_name, :last_name]
end
class Book < ApplicationRecord
belongs_to :author, foreign_key: [:author_first_name, :author_last_name]
end
이 설정에서 Author
는 [:first_name, :last_name]
으로 구성된 composite primary key를 가지며, Book
은 composite foreign key [:author_first_name, :author_last_name]
를 가진 Author
에 belongs to 관계입니다.
Author
와 그와 연관된 Book
을 생성합니다:
author = Author.create!(first_name: "Jane", last_name: "Doe")
book = author.books.create!(title: "A Cool Book", author_first_name: "Jane", author_last_name: "Doe")
book의 author를 접근하기 위해서는, 해당 association을 reload 합니다:
book.reload.author
Rails는 이제 SQL 쿼리에서 composite primary key의 :first_name
과 :last_name
둘 다를 사용할 것입니다.
SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'
6 복합 기본 키 모델을 위한 폼
복합 기본 키 모델을 위한 폼도 만들 수 있습니다. form builder 구문에 대한 자세한 내용은 Form Helpers 가이드를 참조하세요.
복합 키 [:author_id, :id]
를 가진 @book
모델 객체가 있다고 가정해봅시다:
@book = Book.find([2, 25])
# => #<Book id: 25, title: "Some book", author_id: 2>
다음 form:
<%= form_with model: @book do |form| %>
<%= form.text_field :title %>
<%= form.submit %>
<% end %>
네, 입력할 Rails 가이드 문서를 보내주시면 가이드라인에 맞춰서 한국어로 번역해드리겠습니다.
<form action="/books/2_25" method="post" accept-charset="UTF-8" >
<input name="authenticity_token" type="hidden" value="..." />
<input type="text" name="book[title]" id="book_title" value="내 책" />
<input type="submit" name="commit" value="책 업데이트" data-disable-with="책 업데이트">
</form>
생성된 URL에는 밑줄로 구분된 author_id
와 id
가 포함되어 있습니다. 제출되면 컨트롤러는 매개변수에서 기본 키 값을 추출하여 레코드를 업데이트할 수 있습니다. 자세한 내용은 다음 섹션을 참조하세요.
7 Composite Key Parameters
Composite key 매개변수는 하나의 매개변수에 여러 값이 포함됩니다.
이러한 이유로, 각 값을 추출하여 Active Record에 전달할 수 있어야 합니다. 이 사용 사례에서는 extract_value
메서드를 활용할 수 있습니다.
다음과 같은 컨트롤러가 있다고 가정해봅시다:
class BooksController < ApplicationController
def show
# URL 파라미터에서 복합 ID 값을 추출합니다.
id = params.extract_value(:id)
# 복합 ID를 사용하여 책을 찾습니다.
@book = Book.find(id)
# show view를 렌더링하기 위해 기본 렌더링 동작을 사용합니다.
end
end
그리고 다음과 같은 라우트입니다:
get "/books/:id", to: "books#show"
이것은 books#show
action을 사용하여 요청을 books controller로 연결하며, ID를 통해 특정 책을 보여줍니다.
URL /books/4_2
를 사용자가 열면, controller는 복합 키 값 ["4", "2"]
을 추출하여 Book.find
에 전달해 view에서 올바른 레코드를 렌더링합니다. extract_value
메서드는 구분된 매개변수에서 배열을 추출하는 데 사용될 수 있습니다.
8 Composite Primary Key Fixtures
복합 primary key 테이블의 fixture는 일반 테이블과 매우 유사합니다. id 컬럼을 사용할 때는 평소처럼 해당 컬럼을 생략할 수 있습니다:
class Book < ApplicationRecord
self.primary_key = [:author_id, :id]
belongs_to :author
end
# books.yml
alices_adventure_in_wonderland:
author_id: <%= ActiveRecord::FixtureSet.identify(:lewis_carroll) %>
title: "이상한 나라의 앨리스"
하지만 복합 primary key 관계를 지원하기 위해서는 composite_identify
메서드를 사용해야 합니다:
class BookOrder < ApplicationRecord
self.primary_key = [:shop_id, :id]
belongs_to :order, foreign_key: [:shop_id, :order_id]
belongs_to :book, foreign_key: [:author_id, :book_id]
end
# book_orders.yml
alices_adventure_in_wonderland_in_books:
author: lewis_carroll
book_id: <%= ActiveRecord::FixtureSet.composite_identify(
:alices_adventure_in_wonderland, Book.primary_key)[:id] %>
shop: book_store
order_id: <%= ActiveRecord::FixtureSet.composite_identify(
:books, Order.primary_key)[:id] %>