1 Associations 개요
Active Record associations를 사용하면 모델 간의 관계를 정의할 수 있습니다. Associations는 특별한 매크로 스타일 호출로 구현되어 Rails에게 모델들이 서로 어떻게 연관되어 있는지 쉽게 알려줄 수 있으며, 이는 데이터를 더 효과적으로 관리하고 일반적인 작업을 더 단순하고 읽기 쉽게 만드는 데 도움이 됩니다.
매크로 스타일 호출은 런타임에 다른 메서드를 생성하거나 수정하는 메서드로, Rails에서 모델 associations를 정의하는 것과 같이 기능을 간결하고 표현력 있게 선언할 수 있게 합니다. 예를 들어, has_many :comments
와 같습니다.
association을 설정하면 Rails는 두 모델 인스턴스 간의 Primary Key와 Foreign Key 관계를 정의하고 관리하는 데 도움을 주며, 데이터베이스는 데이터가 일관성을 유지하고 적절하게 연결되도록 보장합니다.
이를 통해 어떤 레코드들이 서로 연관되어 있는지 쉽게 추적할 수 있습니다. 또한 모델에 유용한 메서드들을 추가하여 연관된 데이터를 더 쉽게 다룰 수 있게 해줍니다.
저자와 책을 위한 모델이 있는 간단한 Rails 애플리케이션을 고려해봅시다.
1.1 연관관계 없는 경우
연관관계가 없으면 author에 대한 book을 생성하고 삭제하는 것은 지루하고 수동적인 프로세스가 필요합니다. 그 모습은 다음과 같습니다:
class CreateAuthors < ActiveRecord::Migration[8.1]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.references :author
t.datetime :published_at
t.timestamps
end
end
end
class Author < ApplicationRecord
end
class Book < ApplicationRecord
end
기존 author에 새로운 book을 추가하려면, book을 생성할 때 author_id
값을 제공해야 합니다.
@book = Book.create(author_id: @author.id, published_at: Time.now)
이것은 author_id가 @author.id이고 published_at이 현재 시간인 새로운 Book 레코드를 생성합니다.
author를 삭제하고 그들의 모든 book도 삭제하려면, author의 모든 books
를 가져와서 각 book
을 반복문으로 돌면서 삭제한 다음, author를 삭제해야 합니다.
@books = Book.where(author_id: @author.id)
@books.each do |book|
book.destroy
end
@author.destroy
1.2 연관관계 사용하기
연관관계를 사용하면 Rails에 두 모델 간의 관계를 명시적으로 알려줌으로써 이러한 작업들과 다른 작업들을 간소화할 수 있습니다. 다음은 연관관계를 사용하여 author와 book을 설정하는 개선된 코드입니다:
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
belongs_to :author
end
이 변경으로 특정 author에 대한 새로운 book을 생성하는 것이 더 간단해집니다:
@book = @author.books.create(published_at: Time.now)
author와 관련된 모든 book을 삭제하는 것은 훨씬 쉽습니다:
@author.destroy
Rails에서 association을 설정할 때, 데이터베이스가 association을 제대로 처리할 수 있도록 migration을 생성해야 합니다. 이 migration은 데이터베이스 테이블에 필요한 foreign key 컬럼들을 추가해야 합니다.
예를 들어, Book
모델에 belongs_to :author
association을 설정한다면, books
테이블에 author_id
컬럼을 추가하는 migration을 생성해야 합니다:
rails generate migration AddAuthorToBooks author:references
이 migration은 author_id
컬럼을 추가하고 데이터베이스에 foreign key 관계를 설정하여 모델과 데이터베이스가 동기화된 상태를 유지하도록 합니다.
다양한 association 타입에 대해 더 알아보려면 이 가이드의 다음 섹션을 읽어보세요. 그 다음으로 association 작업을 위한 팁과 요령을 찾을 수 있습니다. 마지막으로 Rails의 association 메서드와 옵션에 대한 완전한 레퍼런스가 있습니다.
2 Association의 종류
Rails는 각각 특정 사용 사례를 위한 6가지 타입의 association을 지원합니다.
다음은 지원되는 모든 타입의 목록과 사용 방법, 메서드 파라미터 등에 대한 자세한 정보를 제공하는 API 문서 링크입니다.
이 가이드의 나머지 부분에서는 다양한 형태의 association을 선언하고 사용하는 방법을 배우게 됩니다. 먼저 각 association 타입이 적절한 상황을 간단히 살펴보겠습니다.
2.1 belongs_to
belongs_to
association은 다른 모델과의 관계를 설정하여, 선언된 모델의 각 인스턴스가 다른 모델의 한 인스턴스에 "속하도록" 합니다. 예를 들어, 애플리케이션에 저자와 책이 있고 각 책이 정확히 하나의 저자에게 할당될 수 있다면, 책 모델을 다음과 같이 선언할 수 있습니다:
class Book < ApplicationRecord
belongs_to :author
end
belongs_to
관계는 반드시 단수형을 사용해야 합니다. Book
모델에서 belongs_to :authors
와 같이 복수형을 사용하고 Book.create(authors: @author)
로 책을 생성하려고 하면, Rails는 "uninitialized constant Book::Authors" 에러를 발생시킬 것입니다. 이는 Rails가 자동으로 association 이름으로부터 클래스 이름을 유추하기 때문에 발생합니다. association 이름이 :authors
이면, Rails는 Author
대신 Authors
라는 이름의 클래스를 찾게 됩니다.
해당하는 migration은 다음과 같을 수 있습니다:
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
데이터베이스 관점에서, belongs_to
association은 이 모델의 테이블이 다른 테이블에 대한 참조를 나타내는 컬럼을 포함한다는 것을 의미합니다. 이는 설정에 따라 일대일 또는 일대다 관계를 설정하는 데 사용될 수 있습니다. 일대일 관계에서 다른 클래스의 테이블이 참조를 포함하는 경우, 대신 has_one
을 사용해야 합니다.
단독으로 사용될 때, belongs_to
는 단방향 일대일 관계를 생성합니다. 따라서 위 예시에서 각 book은 자신의 author를 "알고" 있지만, author는 자신의 book에 대해 알지 못합니다. 양방향 association을 설정하려면 - 다른 모델(이 경우 Author 모델)에서 has_one
또는 has_many
와 함께 belongs_to
를 사용하세요.
기본적으로 belongs_to
는 참조 일관성을 보장하기 위해 연관된 레코드의 존재 여부를 검증합니다.
모델에서 optional
이 true로 설정된 경우, belongs_to
는 참조 일관성을 보장하지 않습니다. 이는 한 테이블의 외래 키가 참조된 테이블의 유효한 기본 키를 안정적으로 가리키지 않을 수 있다는 것을 의미합니다.
class Book < ApplicationRecord
belongs_to :author, optional: true
end
따라서, 사용 사례에 따라, 다음과 같이 reference 컬럼에 database-level의 foreign key constraint를 추가해야 할 수도 있습니다:
create_table :books do |t|
t.belongs_to :author, foreign_key: true
# ...
end
참고: 이 migration은 books 테이블에 author_id 컬럼과 해당 foreign key constraint를 추가합니다.
optional: true
가 author_id
가 NULL이 되는 것을 허용하더라도, NULL이 아닐 때는 반드시 authors 테이블의 유효한 레코드를 참조해야 함을 보장합니다.
2.1.1 belongs_to
가 추가하는 메서드들
belongs_to
관계를 선언하면, 선언한 클래스는 자동으로 관계와 관련된 수많은 메서드들을 얻게 됩니다. 그 중 일부는 다음과 같습니다:
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
association_changed?
association_previously_changed?
일반적으로 사용되는 메서드들 중 일부를 살펴보겠지만, 전체 목록은 ActiveRecord Associations API에서 확인할 수 있습니다.
위의 모든 메서드에서, association
은 belongs_to
의 첫 번째 인자로 전달된 심볼로 대체됩니다. 예를 들어, 다음과 같은 선언이 있다고 할 때:
# app/models/book.rb
class Book < ApplicationRecord
belongs_to :author
end
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
validates :name, presence: true
end
Book
모델의 인스턴스는 다음과 같은 메서드들을 가집니다:
author
author=
build_author
create_author
create_author!
reload_author
reset_author
author_changed?
author_previously_changed?
새로운 has_one
또는 belongs_to
association을 초기화할 때는 has_many
나 has_and_belongs_to_many
association에서 사용되는 association.build
메서드 대신 build_
접두사를 사용해야 합니다. 생성하려면 create_
접두사를 사용하세요.
2.1.1.1 association 조회하기
association
메서드는 연관된 객체가 있다면 그 객체를 반환합니다. 연관된 객체가 없으면 nil
을 반환합니다.
@author = @book.author
연관된 객체가 이미 이 객체의 database에서 검색된 경우, 캐시된 버전이 반환됩니다. 이 동작을 재정의하고(그리고 database 읽기를 강제하기) 위해서는 부모 객체에서 #reload_association
을 호출하세요.
@author = @book.reload_author
이는 데이터베이스에서 관계된 author 레코드를 다시 로드하고 해당 object를 반환합니다.
관련된 객체의 캐시된 버전을 언로드하고 다음 접근 시에 데이터베이스에서 다시 조회하도록 하려면 부모 객체에서 #reset_association
을 호출하세요.
@book.reset_author
2.1.1.2 Association 할당하기
association=
메서드는 연관된 객체를 이 객체에 할당합니다. 내부적으로는 연관된 객체에서 primary key를 추출하고 이 객체의 foreign key를 동일한 값으로 설정하는 것을 의미합니다.
@book.author = @author
build_association
메서드는 관련된 타입의 새로운 객체를 반환합니다. 이 객체는 전달된 attributes로 인스턴스화되며, 이 객체의 foreign key를 통한 연결이 설정됩니다. 하지만 관련 객체는 아직 저장되지 않습니다.
@author = @book.build_author(author_number: 123,
author_name: "John Doe")
create_association
메서드는 한 단계 더 나아가서 연관된 모델에 명시된 모든 validation을 통과하면 연관된 객체도 함께 저장합니다.
@author = @book.create_author(author_number: 123,
author_name: "John Doe")
마지막으로 create_association!
은 동일한 작업을 수행하지만, record가 유효하지 않은 경우 ActiveRecord::RecordInvalid
를 발생시킵니다.
# 이름이 비어있기 때문에 ActiveRecord::RecordInvalid가 발생합니다
begin
@book.create_author!(author_number: 123, name: "")
rescue ActiveRecord::RecordInvalid => e
puts e.message
end
irb> raise_validation_error: Validation failed: Name은 비워둘 수 없습니다 (ActiveRecord::RecordInvalid)
2.1.1.3 Association 변경사항 확인하기
association_changed?
메서드는 새로운 연관 객체가 할당되었고 다음 저장 시 foreign key가 업데이트될 예정인 경우 true를 반환합니다.
association_previously_changed?
메서드는 이전 저장이 새로운 연관 객체를 참조하도록 association을 업데이트한 경우 true를 반환합니다.
@book.author # => #<Author author_number: 123, author_name: "John Doe">
@book.author_changed? # => false
@book.author_previously_changed? # => false
@book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith">
@book.author_changed? # => true
@book.save!
@book.author_changed? # => false
@book.author_previously_changed? # => true
model.association_changed?
와 model.association.changed?
를 혼동하지 마세요. 전자는 association이 새로운 레코드로 교체되었는지를 확인하는 반면, 후자는 association의 속성에 대한 변경사항을 추적합니다.
2.1.1.4 기존 Association의 존재 여부 확인하기
association.nil?
메서드를 사용하여 관련된 객체가 존재하는지 확인할 수 있습니다:
if @book.author.nil?
@msg = "이 책의 저자를 찾을 수 없습니다"
end
2.1.1.5 연관된 객체의 저장 동작
belongs_to
association에 객체를 할당하는 것은 현재 객체나 연관된 객체를 자동으로 저장하지 않습니다. 하지만 현재 객체를 저장할 때에는 association도 함께 저장됩니다.
2.2 has_one
has_one
association은 다른 하나의 model이 이 model을 참조한다는 것을 나타냅니다. 해당 model은 이 association을 통해 가져올 수 있습니다.
예를 들어, 애플리케이션의 각 supplier가 하나의 account만 가지고 있다면, supplier model을 다음과 같이 선언할 수 있습니다:
class Supplier < ApplicationRecord
has_one :account
end
has_one
의 belongs_to
와의 주요 차이점은 링크 컬럼(이 경우 supplier_id
)이 has_one
이 선언된 테이블이 아닌 다른 테이블에 위치한다는 것입니다.
해당하는 migration은 다음과 같을 수 있습니다:
class CreateSuppliers < ActiveRecord::Migration[8.1]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
end
end
has_one
association은 다른 모델과 일대일 매칭을 생성합니다. 데이터베이스 측면에서 이 association은 다른 클래스가 foreign key를 포함한다는 것을 의미합니다. 만약 이 클래스가 foreign key를 포함한다면, 대신 belongs_to
를 사용해야 합니다.
사용 사례에 따라 accounts 테이블의 supplier 컬럼에 unique index 그리고/또는 foreign key 제약조건을 생성해야 할 수도 있습니다. unique index는 각 supplier가 하나의 account와만 연관되도록 보장하고 효율적인 방식으로 쿼리할 수 있게 해주며, foreign key 제약조건은 accounts
테이블의 supplier_id
가 suppliers
테이블의 유효한 supplier
를 참조하도록 보장합니다. 이는 데이터베이스 레벨에서 association을 강제합니다.
create_table :accounts do |t|
t.belongs_to :supplier, index: { unique: true }, foreign_key: true
# ...
end
이렇게 하면 supplier_id 칼럼과 unique index, foreign key constraint가 accounts 테이블에 생성됩니다.
이 관계는 다른 모델에서 belongs_to
와 함께 사용될 때 양방향이 될 수 있습니다.
2.2.1 has_one
에 의해 추가되는 메서드
has_one
연관을 선언하면, 선언하는 클래스는 자동으로 연관과 관련된 수많은 메서드를 얻게 됩니다. 그 중 일부는 다음과 같습니다:
association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
일반적인 메서드 몇 가지를 살펴보겠지만, 전체 목록은 ActiveRecord Associations API에서 확인할 수 있습니다.
belongs_to
참조와 마찬가지로, 이러한 모든 메서드에서 association
은 has_one
의 첫 번째 인자로 전달된 심볼로 대체됩니다. 예를 들어, 다음과 같은 선언이 있다고 할 때:
# app/models/supplier.rb
class Supplier < ApplicationRecord
has_one :account
end
# app/models/account.rb
class Account < ApplicationRecord
validates :terms, presence: true
belongs_to :supplier
end
Supplier
모델의 각 인스턴스는 다음과 같은 메서드를 가집니다:
account
account=
build_account
create_account
create_account!
reload_account
reset_account
새로운 has_one
또는 belongs_to
관계를 초기화할 때는 has_many
나 has_and_belongs_to_many
관계에서 사용되는 association.build
메서드 대신 build_
접두사를 사용해야 합니다. 생성하려면 create_
접두사를 사용하세요.
2.2.1.1 관계 조회하기
association
메서드는 연관된 객체가 있다면 그것을 반환합니다. 연관된 객체가 없으면 nil
을 반환합니다.
@account = @supplier.account
해당 객체에 대한 관련 객체가 이미 데이터베이스에서 조회된 경우, 캐시된 버전이 반환됩니다. 이러한 동작을 재정의하고(그리고 데이터베이스 읽기를 강제하기) 위해서는 부모 객체에서 #reload_association
을 호출하세요.
@account = @supplier.reload_account
@supplier의 account를 데이터베이스에서 다시 로드하고, 변수에 할당합니다.
연관된 객체의 캐시된 버전을 unload하고, 이후 액세스할 때 데이터베이스로부터 다시 쿼리하도록 하려면 부모 객체에서 #reset_association
을 호출하세요.
@supplier.reset_account
2.2.1.2 Association 할당하기
association=
메서드는 연결된 객체를 이 객체에 할당합니다. 내부적으로는 이 객체의 primary key를 추출하여 연결된 객체의 foreign key를 동일한 값으로 설정하는 것을 의미합니다.
@supplier.account = @account
build_association
메서드는 관계된 타입의 새로운 객체를 반환합니다. 이 객체는 전달된 attributes로부터 인스턴스화되며, 이 객체의 foreign key를 통한 연결이 설정됩니다. 하지만 관계된 객체는 아직 저장되지 않습니다.
@account = @supplier.build_account(terms: "Net 30")
@supplier 연관된 새로운 account 객체를 생성합니다. account는 저장되지 않습니다.
create_association
메서드는 한 단계 더 나아가 관계된 모델에 명시된 모든 validation을 통과하면 관계된 객체를 저장합니다.
@account = @supplier.create_account(terms: "Net 30")
has_one
관계에 대한 Object를 생성하고 저장할 때 사용할 수 있습니다. 이 메소드는 대응되는 account를 생성하고 저장하며, supplier와 새로 생성된 account 사이의 foreign key를 설정합니다.
마지막으로 create_association!
은 위의 create_association
와 동일하게 동작하지만, record가 유효하지 않은 경우 ActiveRecord::RecordInvalid
를 발생시킵니다.
# terms가 비어있으므로 ActiveRecord::RecordInvalid가 발생할 것입니다
begin
@supplier.create_account!(terms: "")
rescue ActiveRecord::RecordInvalid => e
puts e.message
end
irb> raise_validation_error: Validation 실패: Terms는 비어있을 수 없습니다 (ActiveRecord::RecordInvalid)
2.2.1.3 기존 Association 체크하기
association.nil?
메서드를 사용하여 관련된 객체가 존재하는지 확인할 수 있습니다:
if @supplier.account.nil?
@msg = "이 공급업체에 대한 계정을 찾을 수 없습니다"
end
2.2.1.4 연관된 객체들의 저장 동작
has_one
연관관계에 객체를 할당할 때, 해당 객체는 foreign key를 업데이트하기 위해 자동으로 저장됩니다. 또한 교체되는 객체도 foreign key가 변경되므로 자동으로 저장됩니다.
유효성 검사 오류로 인해 이러한 저장이 실패하면, 할당 구문은 false
를 반환하고 할당 자체가 취소됩니다.
부모 객체(has_one
연관관계를 선언한 객체)가 저장되지 않은 상태라면(new_record?
가 true
를 반환하는 경우), 자식 객체들은 즉시 저장되지 않습니다. 부모 객체가 저장될 때 자동으로 저장됩니다.
객체를 저장하지 않고 has_one
연관관계에 할당하려면, build_association
메서드를 사용하세요. 이 메서드는 연관된 객체의 새로운, 저장되지 않은 인스턴스를 생성하여 저장 여부를 결정하기 전에 작업할 수 있게 해줍니다.
모델에 대한 연관된 객체들의 저장 동작을 제어하고 싶을 때는 autosave: false
를 사용하세요. 이 설정은 부모 객체가 저장될 때 연관된 객체가 자동으로 저장되는 것을 방지합니다. 반면에, 저장되지 않은 연관 객체로 작업하고 준비가 될 때까지 지속성을 지연시키고 싶을 때는 build_association
을 사용하세요.
2.3 has_many
has_many
는 has_one
과 유사하지만 다른 모델과의 일대다 관계를 나타냅니다. 이 association은 주로 belongs_to
association의 "반대쪽"에서 발견됩니다. 이 association은 모델의 각 인스턴스가 다른 모델의 0개 이상의 인스턴스를 가질 수 있음을 나타냅니다. 예를 들어, author와 book이 있는 애플리케이션에서 author 모델은 다음과 같이 선언될 수 있습니다:
class Author < ApplicationRecord
has_many :books
end
has_many
는 모델 간의 일대다 관계를 설정하여, 선언하는 모델(Author
)의 각 인스턴스가 연관된 모델(Book
)의 여러 인스턴스를 가질 수 있도록 합니다.
has_one
과 belongs_to
연관관계와 달리, has_many
연관관계를 선언할 때는 다른 모델의 이름이 복수형이 됩니다.
해당하는 migration은 다음과 같을 수 있습니다:
class CreateAuthors < ActiveRecord::Migration[8.1]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
has_many
association은 다른 model과 일대다 관계를 생성합니다. 데이터베이스 관점에서 이 association은 다른 클래스가 이 클래스의 인스턴스를 참조하는 foreign key를 가질 것이라는 것을 의미합니다.
이 migration에서는 author의 이름을 저장하기 위한 name
컬럼을 가진 authors
테이블이 생성됩니다. books
테이블도 생성되며, belongs_to :author
association을 포함합니다. 이 association은 books
와 authors
테이블 간의 foreign key 관계를 설정합니다. 구체적으로, books
테이블의 author_id
컬럼은 authors
테이블의 id
컬럼을 참조하는 foreign key로 작동합니다. books
테이블에 이 belongs_to :author
association을 포함함으로써, 각 book이 하나의 author와 연결되도록 보장하며, Author
model로부터 has_many
association이 가능하게 됩니다. 이 설정을 통해 각 author가 여러 개의 관련 book을 가질 수 있습니다.
사용 사례에 따라, books 테이블의 author 컬럼에 non-unique index를 생성하고 선택적으로 foreign key 제약조건을 추가하는 것이 일반적으로 좋은 방법입니다. author_id
컬럼에 index를 추가하면 특정 author와 관련된 book을 검색할 때 쿼리 성능이 향상됩니다.
데이터베이스 수준에서 referential integrity를 강제하고 싶다면, 위의 reference
컬럼 선언에 foreign_key: true
옵션을 추가하세요. 이렇게 하면 books 테이블의 author_id
가 반드시 authors
테이블의 유효한 id
와 대응되도록 보장됩니다.
create_table :books do |t|
t.belongs_to :author, index: true, foreign_key: true
# ...
end
윗 부분은 다음의 코드와 동일합니다:
create_table :books do |t|
t.integer :author_id, index: true
# ...
end
이 관계는 다른 모델에서 belongs_to
와 함께 사용될 때 양방향이 될 수 있습니다.
2.3.1 has_many
에 의해 추가되는 메서드
has_many
관계를 선언하면, 선언하는 클래스는 관계와 관련된 많은 메서드들을 얻게 됩니다. 그 중 일부는 다음과 같습니다:
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
일반적인 메서드들 중 일부를 살펴보겠지만, 전체 목록은 ActiveRecord Associations API에서 찾을 수 있습니다.
이 모든 메서드에서 collection
은 has_many
의 첫 번째 인자로 전달된 심볼로 대체되며, collection_singular
는 해당 심볼의 단수형으로 대체됩니다. 예를 들어, 다음과 같은 선언이 있다면:
class Author < ApplicationRecord
has_many :books
end
Author
모델의 인스턴스는 다음과 같은 메서드들을 가질 수 있습니다:
books
books<<(object, ...)
books.delete(object, ...)
books.destroy(object, ...)
books=(objects)
book_ids
book_ids=(ids)
books.clear # collection을 비움
books.empty? # collection이 비어있는지 확인
books.size # collection의 크기를 반환
books.find(...) # collection에서 object를 찾음
books.where(...) # collection에서 조건에 맞는 object를 찾음
books.exists?(...) # collection에 object가 존재하는지 확인
books.build(attributes = {}, ...) # collection에 새로운 object를 생성하지만 저장하지는 않음
books.create(attributes = {}) # collection에 새로운 object를 생성하고 저장
books.create!(attributes = {}) # collection에 새로운 object를 생성하고 저장(실패 시 예외 발생)
books.reload # collection을 다시 로드
2.3.1.1 Collection 관리하기
collection
메서드는 모든 연관된 객체들의 Relation을 반환합니다. 만약 연관된 객체가 없다면 빈 Relation을 반환합니다.
@books = @author.books
@books에 @author의 books가 할당됩니다.
collection.delete
메서드는 객체들의 foreign key를 NULL
로 설정하여 collection에서 하나 이상의 객체를 제거합니다.
@author.books.delete(@book1)
이는 author와 book 사이의 관계를 제거하지만 데이터베이스에서 book 자체를 삭제하지는 않습니다.
추가적으로 객체들이 dependent: :destroy
와 연관되어 있다면 destroy되고, dependent: :delete_all
와 연관되어 있다면 delete됩니다.
collection.destroy
메서드는 각 객체에 대해 destroy
를 실행하여 컬렉션에서 하나 이상의 객체를 제거합니다.
@author.books.destroy(@book1)
:dependent
옵션과 관계없이 객체는 항상 데이터베이스에서 제거됩니다.
collection.clear
메소드는 dependent
옵션에서 지정된 전략에 따라 컬렉션에서 모든 객체를 제거합니다. 옵션이 지정되지 않은 경우 기본 전략을 따릅니다. has_many :through
관계의 기본 전략은 delete_all
이며, has_many
관계의 경우 foreign key를 NULL
로 설정하는 것입니다.
@author.books.clear
연관 객체들을 컬렉션에서 제거합니다. 실제로 records는 삭제되지 않습니다.
객체들이 dependent: :destroy
또는 dependent: :destroy_async
와 연결되어 있다면, dependent: :delete_all
과 마찬가지로 삭제됩니다.
collection.reload
메서드는 모든 관련 객체들의 Relation을 반환하며, 데이터베이스 읽기를 강제로 수행합니다. 관련 객체가 없다면 빈 Relation을 반환합니다.
@books = @author.books.reload
author의 books를 데이터베이스에서 다시 로드하여 @books 변수에 할당합니다.
2.3.1.2 Collection 할당하기
collection=(objects)
메서드는 필요에 따라 추가와 삭제를 통해 collection이 제공된 객체들만 포함하도록 만듭니다. 이 변경사항들은 데이터베이스에 영구 저장됩니다.
collection_singular_ids=(ids)
메서드는 필요에 따라 추가와 삭제를 통해 collection이 제공된 primary key 값으로 식별되는 객체들만 포함하도록 만듭니다. 이 변경사항들은 데이터베이스에 영구 저장됩니다.
2.3.1.3 Collection 쿼리하기
collection_singular_ids
메서드는 collection에 있는 객체들의 id들의 배열을 반환합니다.
@book_ids = @author.book_ids
해당 author와 관련된 모든 book의 id를 가져옵니다.
collection.empty?
메서드는 컬렉션이 연관된 객체를 하나도 포함하지 않은 경우 true
를 반환합니다.
<% if @author.books.empty? %>
책을 찾을 수 없습니다
<% end %>
collection.size
메서드는 collection에 있는 객체의 수를 반환합니다.
@book_count = @author.books.size
collection.find
메서드는 collection 테이블 내의 객체들을 찾습니다.
@available_book = @author.books.find(1)
collection.where
메서드는 제공된 조건에 기반하여 컬렉션 내의 객체를 찾지만, 객체가 지연 로딩되어 데이터베이스는 해당 객체에 접근할 때만 쿼리됩니다.
@available_books = @author.books.where(available: true) # 아직 쿼리 실행 안됨
@available_book = @available_books.first # 이제 데이터베이스에 쿼리가 실행됨
collection.exists?
메서드는 컬렉션의 테이블에 제공된 조건을 만족하는 객체가 있는지 확인합니다.
2.3.1.4 연관된 객체 빌드 및 생성하기
collection.build
메서드는 연관된 타입의 새로운 객체 하나 또는 배열을 반환합니다. 객체는 전달된 속성으로부터 인스턴스화되며, foreign key를 통한 연결이 생성됩니다. 하지만 연관된 객체는 아직 저장되지 않습니다.
@book = @author.books.build(published_at: Time.now,
book_number: "A12345")
@books = @author.books.build([
{ published_at: Time.now, book_number: "A12346" },
{ published_at: Time.now, book_number: "A12347" }
])
collection.create
메서드는 관계된 타입의 새로운 객체 또는 객체 배열을 반환합니다. 이 객체들은 전달된 attributes로부터 인스턴스화되고, foreign key를 통한 연결이 생성되며, 관계된 모델에 명시된 모든 validation을 통과하면 관계된 객체가 저장될 것입니다.
@book = @author.books.create(published_at: Time.now,
book_number: "A12345")
@books = @author.books.create([
{ published_at: Time.now, book_number: "A12346" },
{ published_at: Time.now, book_number: "A12347" }
])
collection.create!
는 collection.create
와 동일하게 동작하지만, 레코드가 유효하지 않은 경우 ActiveRecord::RecordInvalid
를 발생시킵니다.
2.3.1.5 객체가 저장되는 시점은?
has_many
연관관계에 객체를 할당하면, 해당 객체는 자동으로 저장됩니다(foreign key를 업데이트하기 위해). 하나의 구문에서 여러 객체를 할당하면, 모두 저장됩니다.
이러한 저장 중 하나라도 유효성 검사 오류로 인해 실패하면, 할당 구문은 false
를 반환하고 할당 자체가 취소됩니다.
부모 객체(has_many
연관관계를 선언한 객체)가 저장되지 않은 상태라면(즉, new_record?
가 true
를 반환하는 경우), 자식 객체들은 추가될 때 저장되지 않습니다. 연관관계의 모든 저장되지 않은 멤버들은 부모가 저장될 때 자동으로 저장됩니다.
객체를 저장하지 않고 has_many
연관관계에 할당하고 싶다면, collection.build
메서드를 사용하세요.
2.4 has_many :through
has_many :through
연관관계는 종종 다른 모델과 다대다 관계를 설정하는 데 사용됩니다. 이 연관관계는 선언하는 모델이 세 번째 모델을 통해 다른 모델의 0개 이상의 인스턴스와 연결될 수 있음을 나타냅니다.
예를 들어, 환자가 의사와의 진료 예약을 하는 의료 시설을 생각해보겠습니다. 관련 연관관계 선언은 다음과 같을 수 있습니다:
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
has_many :through
는 모델 간의 다대다 관계를 설정하며, 하나의 모델(Physician)의 인스턴스가 세 번째 "join" 모델(Appointment)을 통해 다른 모델(Patient)의 여러 인스턴스와 연결될 수 있도록 합니다.
해당하는 migration은 다음과 같을 수 있습니다:
class CreateAppointments < ActiveRecord::Migration[8.1]
def change
create_table :physicians do |t|
t.string :name
t.timestamps
end
create_table :patients do |t|
t.string :name
t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
이 migration에서는 name
컬럼을 가진 physicians
와 patients
테이블이 생성됩니다. join 테이블 역할을 하는 appointments
테이블은 physician_id
와 patient_id
컬럼으로 생성되어 physicians
와 patients
간의 many-to-many 관계를 설정합니다.
has_many :through
관계에서 join 테이블에 대해 아래와 같이 composite primary key를 사용하는 것도 고려해볼 수 있습니다:
class CreateAppointments < ActiveRecord::Migration[8.1]
def change
# ...
create_table :appointments, primary_key: [:physician_id, :patient_id] do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
has_many :through
연관관계에서 join 모델들의 컬렉션은 표준 has_many
연관관계 메서드를 사용하여 관리할 수 있습니다. 예를 들어, physician에 patients 목록을 다음과 같이 할당하면:
physician.patients = patients
이것은 모든 patients를 physician에게 할당하고 이전의 관계를 대체합니다.
Rails는 의사와 이전에 연관되지 않았던 새로운 리스트의 모든 환자들에 대해 자동으로 새로운 join 모델을 생성합니다. 또한 의사와 이전에 연관되어 있었지만 새로운 리스트에 포함되지 않은 환자들의 join 레코드는 자동으로 삭제됩니다. 이는 join 모델의 생성과 삭제를 대신 처리함으로써 다대다 관계 관리를 단순화합니다.
join 모델의 자동 삭제는 직접적으로 이루어지며, destroy 콜백은 트리거되지 않습니다. 콜백에 대해 더 자세히 알아보려면 Active Record 콜백 가이드를 참조하세요.
has_many :through
연관관계는 중첩된 has_many
연관관계를 통한 "바로가기" 설정에도 유용합니다. 이는 특히 중간 연관관계를 통해 관련 레코드 컬렉션에 접근해야 할 때 유용합니다.
예를 들어, 문서가 여러 섹션을 가지고 있고 각 섹션이 여러 단락을 가지고 있을 때, 각 섹션을 수동으로 순회하지 않고도 문서의 모든 단락을 간단한 컬렉션으로 가져오고 싶을 수 있습니다.
다음과 같이 has_many :through
연관관계를 설정할 수 있습니다:
class Document < ApplicationRecord
has_many :sections
has_many :paragraphs, through: :sections
end
class Section < ApplicationRecord
belongs_to :document
has_many :paragraphs
end
class Paragraph < ApplicationRecord
belongs_to :section
end
주의: 위의 Ruby 코드는 예시일 뿐이며, 실제로는 model의 association을 정의하는 것입니다.
through: :sections
가 지정되면 Rails는 이제 다음과 같이 이해할 것입니다:
@document.paragraphs
반면에, has_many :through
관계를 설정하지 않았다면 document의 paragraph를 가져오기 위해 다음과 같은 작업을 해야했을 것입니다:
paragraphs = []
@document.sections.each do |section|
paragraphs.concat(section.paragraphs)
end
각 section의 paragraphs를 paragraphs 배열에 추가합니다.
2.5 has_one :through
has_one :through
association은 중간 model을 통해 다른 model과 일대일 관계를 설정합니다. 이 association은 선언하는 model이 제3의 model을 통해 다른 model의 하나의 인스턴스와 매칭될 수 있음을 나타냅니다.
예를 들어, 각 supplier가 하나의 account를 가지고, 각 account가 하나의 account history와 연관되어 있다면, supplier model은 다음과 같이 보일 수 있습니다:
class Supplier < ApplicationRecord
has_one :account
has_one :account_history, through: :account
end
class Account < ApplicationRecord
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ApplicationRecord
belongs_to :account
end
이 설정은 supplier
가 자신의 account
를 통해 직접 account_history
에 접근할 수 있게 해줍니다.
이러한 association을 설정하기 위한 migration은 다음과 같을 수 있습니다:
다음 migration은 3개의 테이블을 생성합니다:
- suppliers 테이블은 name 컬럼을 가집니다.
- accounts 테이블은 supplier와 belongs_to 관계를 가지며, account_number 컬럼을 가집니다.
- account_histories 테이블은 account와 belongs_to 관계를 가지며, credit_rating 컬럼을 가집니다.
모든 테이블에는 timestamps (created_at과 updated_at) 컬럼이 포함됩니다.
2.6 has_and_belongs_to_many
has_and_belongs_to_many
는 중간 model 없이 다른 model과 직접적인 다대다 관계를 생성합니다. 이 association은 선언하는 model의 각 인스턴스가 다른 model의 0개 이상의 인스턴스를 참조할 수 있음을 나타냅니다.
예를 들어, Assembly
와 Part
model이 있는 애플리케이션을 생각해보세요. 여기서 각 assembly는 많은 parts를 포함할 수 있고, 각 part는 많은 assemblies에서 사용될 수 있습니다. 다음과 같이 model을 설정할 수 있습니다:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
has_and_belongs_to_many
는 중간 모델을 필요로 하지는 않지만, 두 모델 간의 다대다 관계를 설정하기 위한 별도의 테이블이 필요합니다. 이 중간 테이블은 관련 데이터를 저장하고, 두 모델의 인스턴스 간의 연결을 매핑하는 역할을 합니다. 이 테이블은 관련 레코드 간의 관계만을 관리하는 것이 목적이므로 반드시 primary key가 필요하지는 않습니다. 해당하는 migration은 다음과 같을 수 있습니다:
class CreateAssembliesAndParts < ActiveRecord::Migration[8.1]
def change
create_table :assemblies do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
# assemblies와 parts 간의 다대다 관계를 설정하기 위한 join 테이블을 생성합니다.
# `id: false`는 테이블에 자체 primary key가 필요하지 않다는 것을 나타냅니다
create_table :assemblies_parts, id: false do |t|
# join 테이블을 `assemblies`와 `parts` 테이블에 연결하는 foreign key를 생성합니다
t.belongs_to :assembly
t.belongs_to :part
end
end
end
has_and_belongs_to_many
association은 다른 모델과 다대다 관계를 생성합니다. 데이터베이스 관점에서 이는 두 클래스의 foreign key를 포함하는 중간 join 테이블을 통해 두 클래스를 연결합니다.
has_and_belongs_to_many
association의 join 테이블이 두 개의 foreign key 외에 추가 컬럼을 가지고 있다면, 이 컬럼들은 해당 association을 통해 검색된 레코드의 속성으로 추가됩니다. 추가 속성이 있는 반환된 레코드는 항상 읽기 전용입니다. Rails가 이러한 속성들의 변경사항을 저장할 수 없기 때문입니다.
has_and_belongs_to_many
association에서 join 테이블의 추가 속성 사용은 deprecated 되었습니다. 다대다 관계에서 두 모델을 연결하는 테이블에 이러한 복잡한 동작이 필요한 경우, has_and_belongs_to_many
대신 has_many :through
association을 사용해야 합니다.
2.6.1 has_and_belongs_to_many
에 의해 추가되는 메서드
has_and_belongs_to_many
association을 선언하면, 선언하는 클래스는 association과 관련된 다수의 메서드를 얻게 됩니다. 그 중 일부는 다음과 같습니다:
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
일반적인 메서드들을 살펴보겠지만, 전체 목록은 ActiveRecord Associations API에서 확인할 수 있습니다.
이 모든 메서드에서 collection
은 has_and_belongs_to_many
의 첫 번째 인자로 전달된 심볼로 대체되며, collection_singular
는 해당 심볼의 단수형으로 대체됩니다. 예를 들어, 다음과 같은 선언이 있다면:
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Part
모델의 인스턴스는 다음과 같은 메서드를 가질 수 있습니다:
assemblies
assemblies<<(object, ...) // assemblies에 객체를 추가합니다
assemblies.delete(object, ...) // assemblies에서 객체를 삭제합니다
assemblies.destroy(object, ...) // assemblies에서 객체를 파괴합니다
assemblies=(objects) // assemblies를 objects로 설정합니다
assembly_ids // assembly id들을 반환합니다
assembly_ids=(ids) // assembly id들을 설정합니다
assemblies.clear // assemblies를 비웁니다
assemblies.empty? // assemblies가 비어있는지 확인합니다
assemblies.size // assemblies의 크기를 반환합니다
assemblies.find(...) // assemblies에서 찾습니다
assemblies.where(...) // assemblies에서 조건에 맞는 것을 찾습니다
assemblies.exists?(...) // assemblies에 해당 객체가 존재하는지 확인합니다
assemblies.build(attributes = {}, ...) // 새로운 assembly를 빌드합니다
assemblies.create(attributes = {}) // 새로운 assembly를 생성합니다
assemblies.create!(attributes = {}) // 새로운 assembly를 생성합니다 (실패시 예외 발생)
assemblies.reload // assemblies를 새로고침합니다
2.6.1.1 컬렉션 관리하기
collection
메서드는 모든 관련 객체들의 Relation을 반환합니다. 관련된 객체가 없는 경우에는 빈 Relation을 반환합니다.
@assemblies = @part.assemblies
collection<<
메서드는 join 테이블에 레코드를 생성하여 하나 이상의 객체를 collection에 추가합니다.
@part.assemblies << @assembly1
이 메서드는 collection.concat
과 collection.push
의 별칭입니다.
collection.delete
메서드는 join 테이블의 레코드를 삭제하여 컬렉션에서 하나 이상의 객체를 제거합니다. 이는 객체 자체를 destroy하지는 않습니다.
@part.assemblies.delete(@assembly1)
assembly에서 특정 part를 삭제합니다.
collection.destroy
메소드는 join 테이블의 레코드를 삭제하여 컬렉션에서 하나 이상의 객체를 제거합니다. 이는 객체 자체를 destroy하지는 않습니다.
@part.assemblies.destroy(@assembly1)
이는 join 테이블에서 연관된 레코드를 삭제하고 두 객체를 분리시키지만, 객체 자체는 삭제하지 않습니다.
collection.clear
메서드는 조인 테이블에서 행을 삭제하여 컬렉션의 모든 객체를 제거합니다. 이는 연결된 객체들을 파괴하지는 않습니다.
2.6.1.2 컬렉션 할당하기
collection=
메서드는 필요에 따라 추가 및 삭제를 통해 컬렉션이 제공된 객체들만 포함하도록 만듭니다. 변경사항은 데이터베이스에 영속화됩니다.
collection_singular_ids=
메서드는 필요에 따라 추가 및 삭제를 통해 컬렉션이 제공된 primary key 값으로 식별된 객체들만 포함하도록 만듭니다. 변경사항은 데이터베이스에 영속화됩니다.
2.6.1.3 컬렉션 쿼리하기
collection_singular_ids
메서드는 컬렉션에 있는 객체들의 id를 배열로 반환합니다.
@assembly_ids = @part.assembly_ids
여기에서 assembly_ids
메소드는 part가 연관된 모든 assembly의 id를 배열로 반환합니다.
collection.empty?
메서드는 collection에 관련된 객체가 하나도 없는 경우 true
를 반환합니다.
<% if @part.assemblies.empty? %>
이 부품은 어떤 조립품에서도 사용되지 않습니다
<% end %>
collection.size
메서드는 컬렉션에 있는 오브젝트의 개수를 반환합니다.
@assembly_count = @part.assemblies.size
collection.find
메서드는 collection의 테이블 내에서 객체들을 찾습니다.
@assembly = @part.assemblies.find(1)
collection.where
메서드는 제공된 조건에 기반하여 collection 내의 객체들을 찾지만, 객체들은 지연 로딩되어 실제로 객체에 접근할 때만 데이터베이스에 쿼리가 실행됩니다.
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
collection.exists?
메서드는 컬렉션의 테이블에 제공된 조건을 충족하는 객체가 존재하는지 확인합니다.
2.6.1.4 연관된 객체 생성하기
collection.build
메서드는 연관된 타입의 새로운 객체를 반환합니다. 이 객체는 전달된 속성들로 인스턴스화되며, join 테이블을 통한 연결이 생성되지만, 연관된 객체는 아직 저장되지 않습니다.
@assembly = @part.assemblies.build({ assembly_name: "트랜스미션 하우징" })
collection.create
메서드는 관련된 타입의 새로운 객체를 반환합니다. 이 객체는 전달된 attributes로 인스턴스화되고, join 테이블을 통한 링크가 생성되며, 관련 모델에 지정된 모든 validation을 통과하면 관련 객체가 저장될 것입니다.
@assembly = @part.assemblies.create({ assembly_name: "변속기 하우징" })
collection.create
와 동일하지만, record가 유효하지 않은 경우 ActiveRecord::RecordInvalid
를 발생시킵니다.
collection.reload
메서드는 모든 관련 객체의 Relation을 반환하며, 데이터베이스에서 강제로 다시 읽어옵니다. 관련된 객체가 없는 경우 빈 Relation을 반환합니다.
@assemblies = @part.assemblies.reload
참조된 assemblies를 데이터베이스에서 리로드합니다.
2.6.1.5 객체는 언제 저장되나요?
객체를 has_and_belongs_to_many
association에 할당하면, 해당 객체는 자동으로 저장됩니다(join table을 업데이트하기 위해). 하나의 statement에서 여러 객체를 할당하면, 모두 저장됩니다.
이러한 저장 중 하나라도 validation error로 인해 실패하면, 할당 statement는 false
를 반환하고 할당 자체가 취소됩니다.
부모 객체(has_and_belongs_to_many
association을 선언한 객체)가 저장되지 않은 상태라면(즉, new_record?
가 true
를 반환하면) 자식 객체들은 추가될 때 저장되지 않습니다. association의 모든 저장되지 않은 멤버들은 부모가 저장될 때 자동으로 저장됩니다.
객체를 저장하지 않고 has_and_belongs_to_many
association에 할당하고 싶다면, collection.build
메서드를 사용하세요.
3 Association 선택하기
3.1 belongs_to
vs has_one
두 모델 간의 일대일 관계를 설정하려는 경우, belongs_to
와 has_one
연관관계 중에서 선택할 수 있습니다. 어떤 것을 선택해야 할까요?
차이점은 foreign key의 위치에 있으며, foreign key는 belongs_to
연관관계를 선언하는 클래스의 테이블에 위치합니다. 하지만 올바른 연관관계를 결정하기 위해서는 의미론적인 이해가 필수적입니다:
belongs_to
: 이 연관관계는 현재 모델이 foreign key를 포함하고 있으며 관계에서 자식임을 나타냅니다. 다른 모델을 참조하며, 이 모델의 각 인스턴스가 다른 모델의 하나의 인스턴스와 연결되어 있음을 의미합니다.has_one
: 이 연관관계는 현재 모델이 관계에서 부모이며, 다른 모델의 하나의 인스턴스를 소유하고 있음을 나타냅니다.
예를 들어, supplier와 그들의 account를 고려해봅시다. supplier가 account를 가지고 있다(supplier가 부모)고 말하는 것이 account가 supplier를 가지고 있다고 말하는 것보다 더 타당합니다. 따라서 올바른 연관관계는 다음과 같습니다:
- supplier는 하나의 account를 가집니다.
- account는 하나의 supplier에 속합니다.
Rails에서 이러한 연관관계를 다음과 같이 정의할 수 있습니다:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
이러한 association들을 구현하기 위해서는 해당하는 데이터베이스 테이블들을 생성하고 foreign key를 설정해야 합니다. 다음은 migration 예시입니다:
class CreateSuppliers < ActiveRecord::Migration[8.1]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier_id
t.string :account_number
t.timestamps
end
add_index :accounts, :supplier_id
end
end
belongs_to 연관관계를 선언하는 클래스의 테이블에 foreign key가 들어간다는 것을 기억하세요. 여기서는 account
테이블입니다.
3.2 has_many :through
vs has_and_belongs_to_many
Rails는 모델 간의 다대다 관계를 선언하는 두 가지 다른 방법을 제공합니다:
has_many :through
와 has_and_belongs_to_many
입니다. 각각의 차이점과 사용 사례를 이해하면 애플리케이션의 요구사항에 가장 적합한 접근 방식을 선택하는 데 도움이 될 수 있습니다.
has_many :through
association은 중간 모델(join model이라고도 함)을 통해 다대다 관계를 설정합니다. 이 접근 방식은 더 유연하며 join model에 validation, callback 및 추가 속성을 추가할 수 있습니다. join 테이블은 primary_key
(또는 composite primary key)가 필요합니다.
class Assembly < ApplicationRecord
has_many :manifests
has_many :parts, through: :manifests
end
class Manifest < ApplicationRecord
belongs_to :assembly
belongs_to :part
end
class Part < ApplicationRecord
has_many :manifests
has_many :assemblies, through: :manifests
end
다음과 같은 경우에 has_many :through
를 사용합니다:
- join 테이블에 추가 속성이나 메서드가 필요한 경우
- join 모델에 validations이나 callbacks이 필요한 경우
- join 테이블이 자체적인 동작을 가진 독립적인 엔티티로 취급되어야 하는 경우
has_and_belongs_to_many
association은 중간 모델 없이 두 모델 간에 직접적으로 many-to-many 관계를 생성할 수 있게 해줍니다. 이 방법은 간단하며 join 테이블에 추가 속성이나 동작이 필요하지 않은 단순한 association에 적합합니다. has_and_belongs_to_many
association의 경우, primary key가 없는 join 테이블을 생성해야 합니다.
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
has_and_belongs_to_many
를 사용하는 경우:
- association이 단순하고 join 테이블에 추가적인 속성이나 동작이 필요하지 않을 때
- join 테이블에 validation, callback 또는 추가적인 메서드가 필요하지 않을 때
4 고급 Association
4.1 Polymorphic Associations
연관관계에서 조금 더 고급스러운 방식은 polymorphic association 입니다. Rails의 polymorphic association은 하나의 연관관계를 통해 모델이 여러 다른 모델들에 속할 수 있도록 해줍니다. 이는 모델이 서로 다른 타입의 모델들과 연결될 필요가 있을 때 특히 유용합니다.
예를 들어, Picture
모델이 Employee
또는 Product
중 하나에 속할 수 있는 상황을 생각해보세요. 각각의 모델이 프로필 사진을 가질 수 있기 때문입니다.
이는 다음과 같이 선언될 수 있습니다:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
위 컨텍스트에서 imageable
은 association을 위해 선택된 이름입니다. 이는 Picture
모델과 Employee
및 Product
와 같은 다른 모델들 간의 polymorphic association을 나타내는 상징적인 이름입니다. 중요한 점은 polymorphic association을 올바르게 설정하기 위해 모든 관련 모델에서 동일한 이름(imageable
)을 일관되게 사용하는 것입니다.
Picture
모델에서 belongs_to :imageable, polymorphic: true
를 선언할 때, Picture
가 이 association을 통해 어떤 모델(예: Employee
나 Product
)에도 속할 수 있다고 명시하는 것입니다.
polymorphic belongs_to
선언은 다른 모델이 사용할 수 있는 인터페이스를 설정하는 것으로 생각할 수 있습니다. 이를 통해 Employee
모델의 인스턴스에서 @employee.pictures
를 사용하여 사진 컬렉션을 가져올 수 있습니다. 마찬가지로 Product
모델의 인스턴스에서 @product.pictures
를 사용하여 사진 컬렉션을 가져올 수 있습니다.
또한 Picture
모델의 인스턴스가 있다면, @picture.imageable
을 통해 그 상위 객체를 가져올 수 있으며, 이는 Employee
나 Product
가 될 수 있습니다.
polymorphic association을 수동으로 설정하려면 모델에 외래 키 컬럼(imageable_id
)과 타입 컬럼(imageable_type
)을 모두 선언해야 합니다:
class CreatePictures < ActiveRecord::Migration[8.1]
def change
create_table :pictures do |t|
t.string :name
t.bigint :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
예시에서 imageable_id
는 Employee
또는 Product
의 ID가 될 수 있고, imageable_type
은 연관된 모델 클래스의 이름이므로 Employee
또는 Product
가 됩니다.
polymorphic association을 수동으로 생성하는 것도 가능하지만, 대신 t.references
또는 그의 별칭인 t.belongs_to
를 사용하고 polymorphic: true
를 지정하는 것이 권장됩니다. 이렇게 하면 Rails가 해당 association이 polymorphic이라는 것을 알고, foreign key와 type 컬럼을 테이블에 자동으로 추가합니다.
class CreatePictures < ActiveRecord::Migration[8.1]
def change
create_table :pictures do |t|
t.string :name
t.belongs_to :imageable, polymorphic: true
t.timestamps
end
end
end
Polymorphic association은 데이터베이스에 클래스 이름을 저장하는 것에 의존하기 때문에, 해당 데이터는 Ruby 코드에서 사용되는 클래스 이름과 동기화된 상태를 유지해야 합니다. 클래스 이름을 변경할 때는 polymorphic type 컬럼의 데이터를 반드시 업데이트해야 합니다.
예를 들어, 클래스 이름을 Product
에서 Item
으로 변경한다면 pictures
테이블(또는 영향을 받는 다른 테이블)의 imageable_type
컬럼을 새로운 클래스 이름으로 업데이트하는 migration 스크립트를 실행해야 합니다. 또한 변경사항을 반영하기 위해 애플리케이션 코드 전반에 있는 클래스 이름에 대한 다른 참조들도 업데이트해야 합니다.
4.2 복합 Primary Key를 가진 Model
Rails는 연관된 model 간의 primary key-foreign key 관계를 추론할 수 있지만, 복합 primary key를 다룰 때는 명시적으로 지정하지 않는 한 일반적으로 복합 키의 일부(주로 id 컬럼)만 사용하도록 기본 설정되어 있습니다.
Rails model에서 복합 primary key를 사용하고 있고 연관 관계를 올바르게 처리해야 하는 경우, Composite Primary Keys 가이드의 Associations 섹션을 참조하세요. 이 섹션에서는 Rails에서 복합 primary key를 사용한 연관 관계 설정 및 사용 방법에 대한 포괄적인 가이드를 제공하며, 필요한 경우 복합 foreign key를 지정하는 방법도 포함하고 있습니다.
4.3 Self Joins
Self join은 일반적인 join이지만, 테이블이 자기 자신과 join되는 것입니다. 이는 단일 테이블 내에 계층적 관계가 있는 상황에서 유용합니다. 일반적인 예시로는 직원이 관리자를 가질 수 있고, 그 관리자도 직원인 직원 관리 시스템이 있습니다.
직원이 다른 직원의 관리자가 될 수 있는 조직을 생각해봅시다. 우리는 단일 employees
테이블을 사용하여 이 관계를 추적하고자 합니다.
Rails 모델에서, 이러한 관계를 반영하기 위해 Employee
클래스를 다음과 같이 정의합니다:
class Employee < ApplicationRecord
# employee는 많은 부하 직원을 가질 수 있습니다.
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
# employee는 하나의 관리자를 가질 수 있습니다.
belongs_to :manager, class_name: "Employee", optional: true
end
has_many :subordinates
는 한 employee가 여러 subordinates를 가질 수 있는 일대다 관계를 설정합니다. 여기서 관련 모델이 역시 Employee
(class_name: "Employee"
)이고 manager를 식별하기 위한 foreign key가 manager_id
임을 지정합니다.
belongs_to :manager
는 한 employee가 한 명의 manager에 속할 수 있는 일대일 관계를 설정합니다. 마찬가지로 관련 모델을 Employee
로 지정합니다.
이 관계를 지원하기 위해서는 employees
테이블에 manager_id
컬럼을 추가해야 합니다. 이 컬럼은 다른 employee(manager)의 id
를 참조합니다.
class CreateEmployees < ActiveRecord::Migration[8.1]
def change
create_table :employees do |t|
# manager에 대한 belongs_to 참조를 추가합니다. manager는 employee입니다.
t.belongs_to :manager, foreign_key: { to_table: :employees }
t.timestamps
end
end
end
t.belongs_to :manager
는employees
테이블에manager_id
컬럼을 추가합니다.foreign_key: { to_table: :employees }
는manager_id
컬럼이employees
테이블의id
컬럼을 참조하도록 보장합니다.
foreign_key
에 전달되는 to_table
옵션과 기타 옵션들은 [SchemaStatements#add_reference
][connection.add_reference]에서 설명됩니다.
이 설정으로 Rails 애플리케이션에서 직원의 부하 직원들과 관리자에 쉽게 접근할 수 있습니다.
직원의 부하 직원들을 가져오려면:
employee = Employee.find(1)
subordinates = employee.subordinates
직원의 매니저를 가져오기 위해서:
manager = employee.manager
5 Single Table Inheritance (STI)
Single Table Inheritance (STI)는 여러 모델을 하나의 데이터베이스 테이블에 저장할 수 있게 해주는 Rails의 패턴입니다. 이는 공통된 속성과 동작을 공유하면서도 특정 동작을 가지는 서로 다른 유형의 엔티티가 있을 때 유용합니다.
예를 들어, Car
, Motorcycle
, Bicycle
모델이 있다고 가정해봅시다. 이 모델들은 color
와 price
같은 필드를 공유하지만, 각각 고유한 동작을 가집니다. 또한 각각 자신만의 컨트롤러를 가지게 됩니다.
5.1 기본 Vehicle 모델 생성하기
먼저, 공통 필드를 가진 기본 Vehicle
모델을 생성합니다:
$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}
여기서 type
필드는 모델 이름(Car
, Motorcycle
, Bicycle
)을 저장하기 때문에 STI에서 매우 중요합니다. STI는 동일한 테이블에 저장된 서로 다른 모델들을 구분하기 위해 이 필드가 필요합니다.
5.2 자식 모델 생성하기
다음으로 Vehicle을 상속받는 Car
, Motorcycle
, Bicycle
모델들을 생성합니다. 이 모델들은 자신만의 테이블을 가지지 않습니다. 대신 vehicles
테이블을 사용합니다.
Car
모델을 생성하려면:
$ bin/rails generate model car --parent=Vehicle
이를 위해, --parent=PARENT
옵션을 사용할 수 있습니다. 이 옵션은 지정된 parent로부터 상속받는 모델을 생성하며 migration은 생성하지 않습니다(테이블이 이미 존재하기 때문에).
이는 Vehicle
을 상속받는 Car
모델을 생성합니다:
class Car < Vehicle
end
이것은 연관관계나 public 메서드 등 Vehicle에 추가된 모든 동작이 Car에서도 사용 가능하다는 것을 의미합니다. Car를 생성하면 vehicles
테이블에 type
필드가 "Car"로 저장됩니다.
Motorcycle
과 Bicycle
에도 동일한 과정을 반복하세요.
5.3 레코드 생성하기
Car
레코드 생성하기:
Car.create(color: "Red", price: 10000)
이는 다음과 같은 SQL을 생성할 것입니다:
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
5.4 레코드 쿼리하기
Car 레코드를 쿼리할 때는 자동차인 차량만 검색됩니다:
Car.all
다음과 같은 쿼리를 실행할 것입니다:
SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
5.5 특정 동작 추가하기
자식 모델에 특정 동작이나 메서드를 추가할 수 있습니다. 예를 들어,
Car
모델에 메서드를 추가하는 방법:
class Car < Vehicle
def honk
"빵빵"
end
end
이제 Car
인스턴스에서 honk
메서드를 호출할 수 있습니다:
car = Car.first
car.honk
# => '빵 빵'
5.6 Controllers
각 model은 자신만의 controller를 가질 수 있습니다. 예를 들어 CarsController
:
# app/controllers/cars_controller.rb
class CarsController < ApplicationController
def index
@cars = Car.all
end
end
5.7 상속 컬럼 오버라이딩하기
레거시 데이터베이스와 작업할 때처럼 상속 컬럼의 이름을 오버라이드해야 하는 경우가 있을 수 있습니다. 이는 inheritance_column method를 사용하여 구현할 수 있습니다.
# Schema: vehicles[ id, kind, created_at, updated_at ]
class Vehicle < ApplicationRecord
self.inheritance_column = "kind"
end
class Car < Vehicle
end
Car.create
# => #<Car kind: "Car", color: "Red", price: 10000>
위 예시에서는 vehicles 테이블의 kind
컬럼을 사용하여 STI(Single Table Inheritance)를 구현합니다.
이 설정에서 Rails는 모델 타입을 저장하기 위해 kind
컬럼을 사용할 것이며, STI가 커스텀 컬럼 이름으로 올바르게 동작하도록 합니다.
5.8 inheritance column 비활성화하기
(레거시 데이터베이스 작업과 같이) Single Table Inheritance를 완전히 비활성화해야 하는 경우가 있을 수 있습니다. STI를 제대로 비활성화하지 않으면 ActiveRecord::SubclassNotFound
에러가 발생할 수 있습니다.
STI를 비활성화하려면 inheritance_column을 nil
로 설정하면 됩니다.
# Schema: vehicles[ id, type, created_at, updated_at ]
class Vehicle < ApplicationRecord
self.inheritance_column = nil
end
Vehicle.create!(type: "Car")
# => #<Vehicle type: "Car", color: "Red", price: 10000>
이 구성에서 Rails는 type 컬럼을 일반 속성으로 취급하며 STI 용도로 사용하지 않습니다. 이는 STI 패턴을 따르지 않는 레거시 스키마와 작업해야 할 때 유용합니다.
이러한 조정은 Rails를 기존 데이터베이스와 통합하거나 모델에 특정 커스터마이징이 필요할 때 유연성을 제공합니다.
5.9 고려사항
Single Table Inheritance (STI)
는 서브클래스들 간의 차이가 적고 속성이 유사할 때 가장 잘 작동하지만, 모든 서브클래스의 모든 속성을 단일 테이블에 포함합니다.
이 접근 방식의 단점은 테이블 비대화를 초래할 수 있다는 것입니다. 테이블이 각 서브클래스에 특정된 속성들을 포함하게 되는데, 이러한 속성들이 다른 클래스에서는 사용되지 않더라도 포함되기 때문입니다. 이는 Delegated Types
를 사용하여 해결할 수 있습니다.
또한, 모델이 type과 ID를 통해 여러 다른 모델에 속할 수 있는 polymorphic associations를 사용하는 경우, 연관 로직이 서로 다른 타입을 올바르게 처리해야 하기 때문에 참조 무결성을 유지하는 것이 복잡해질 수 있습니다.
마지막으로, 서브클래스 간에 서로 다른 특정 데이터 무결성 검사나 유효성 검증이 있는 경우, 특히 외래 키 제약 조건을 설정할 때 이러한 것들이 Rails나 데이터베이스에서 올바르게 처리되도록 보장해야 합니다.
6 Delegated Types
Delegated types는 delegated_type
을 통해 Single Table Inheritance (STI)
의 테이블 비대화 문제를 해결합니다. 이 접근 방식을 통해 공유 속성은 수퍼클래스 테이블에 저장하고 서브클래스 특정 속성은 별도의 테이블에 저장할 수 있습니다.
6.1 Delegated Types 설정하기
Delegated types를 사용하기 위해서는 다음과 같이 데이터를 모델링해야 합니다:
- 모든 하위클래스들 간에 공유되는 속성들을 자신의 테이블에 저장하는 superclass가 있어야 합니다.
- 각 하위클래스는 superclass를 상속받아야 하며, 하위클래스에 특화된 추가 속성들을 위한 별도의 테이블을 가지게 됩니다.
이를 통해 하위클래스들 간에 의도치 않게 공유되는 단일 테이블의 속성들을 정의할 필요가 없어집니다.
6.2 Model 생성하기
위의 예제를 적용하기 위해서는 model을 다시 생성해야 합니다.
먼저 superclass의 역할을 할 기본 Entry
model을 생성해봅시다:
$ bin/rails generate model entry entryable_type:string entryable_id:integer
그 다음, delegation을 위해서 새로운 Message
와 Comment
모델을 생성할 것입니다:
$ bin/rails generate model message subject:string body:string
$ bin/rails generate model comment content:string
제너레이터를 실행한 후, 우리의 모델은 다음과 같이 보일 것입니다:
# Schema: entries[ id, entryable_type, entryable_id, created_at, updated_at ]
class Entry < ApplicationRecord
end
# Schema: messages[ id, subject, body, created_at, updated_at ]
class Message < ApplicationRecord
end
# Schema: comments[ id, content, created_at, updated_at ]
class Comment < ApplicationRecord
end
6.3 delegated_type
선언하기
먼저, 슈퍼클래스 Entry
에 delegated_type
을 선언합니다.
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
end
entryable
파라미터는 위임에 사용할 필드를 지정하며, Message
와 Comment
를 위임 클래스로 포함합니다. entryable_type
과 entryable_id
필드는 각각 위임 서브클래스의 클래스명과 레코드 ID를 저장합니다.
6.4 Entryable
모듈 정의하기
그 다음으로, has_one
연관관계에서 as: :entryable
매개변수를 선언하여 위임된 타입들을 구현하는 모듈을 정의합니다.
module Entryable
extend ActiveSupport::Concern
included do
has_one :entry, as: :entryable, touch: true
end
end
생성한 module을 subclass에서 다음과 같이 include하세요:
class Message < ApplicationRecord
include Entryable
end
class Comment < ApplicationRecord
include Entryable
end
이 정의가 완료되면, 우리의 Entry
delegator는 다음과 같은 메서드들을 제공합니다:
메서드 | 반환값 |
---|---|
Entry.entryable_types |
["Message", "Comment"] |
Entry#entryable_class |
Message 또는 Comment |
Entry#entryable_name |
"message" 또는 "comment" |
Entry.messages |
Entry.where(entryable_type: "Message") |
Entry#message? |
entryable_type == "Message" 일 때 true를 반환 |
Entry#message |
entryable_type == "Message" 일 때 message 레코드를 반환, 그렇지 않으면 nil |
Entry#message_id |
entryable_type == "Message" 일 때 entryable_id 를 반환, 그렇지 않으면 nil |
Entry.comments |
Entry.where(entryable_type: "Comment") |
Entry#comment? |
entryable_type == "Comment" 일 때 true를 반환 |
Entry#comment |
entryable_type == "Comment" 일 때 comment 레코드를 반환, 그렇지 않으면 nil |
Entry#comment_id |
entryable_type == "Comment" 일 때 entryable_id를 반환, 그렇지 않으면 nil |
6.5 Object creation
새로운 Entry
객체를 생성할 때, 동시에 entryable
하위 클래스를 지정할 수 있습니다.
Entry.create! entryable: Message.new(subject: "hello!")
6.6 추가적인 위임 방법
delegate
를 정의하고 subclass에서 polymorphism을 사용하여 Entry
delegator를 향상시킬 수 있습니다. 예를 들어 Entry
의 title
메서드를 subclass에 위임하려면:
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ]
delegate :title, to: :entryable
end
class Message < ApplicationRecord
include Entryable
def title
subject
end
end
class Comment < ApplicationRecord
include Entryable
def title
content.truncate(20)
end
end
위 코드는 한국어로 번역이 필요하지 않은 Ruby 코드입니다.
이 설정은 Entry
가 하위 클래스에 title
메서드를 위임할 수 있게 합니다.
여기서 Message
는 subject
를 사용하고 Comment
는 content
의 잘린 버전을 사용합니다.
7 팁, 트릭, 그리고 주의사항
Rails 애플리케이션에서 Active Record 관계를 효율적으로 사용하기 위해 알아야 할 몇 가지 사항이 있습니다:
- 캐싱 제어하기
- 이름 충돌 피하기
- 스키마 업데이트하기
- 관계 범위 제어하기
- 양방향 관계
7.1 Association 캐싱 제어하기
모든 association 메서드는 캐싱을 기반으로 구축되어 있으며, 이는 로드된 association의 결과를 이후 작업을 위해 유지합니다. 캐시는 메서드 간에도 공유됩니다. 예를 들어:
# 데이터베이스에서 books를 가져옴
author.books.load
# books의 캐시된 복사본을 사용
author.books.size
# books의 캐시된 복사본을 사용
author.books.empty?
author.books
를 사용할 때, 데이터는 즉시 데이터베이스에서 로드되지 않습니다. 대신, 실제로 데이터를 사용하려고 할 때(예: each, size, empty? 등과 같이 데이터가 필요한 메서드를 호출할 때) 실행될 쿼리를 설정합니다. 다른 데이터 사용 메서드를 호출하기 전에 author.books.load
를 호출하면, 데이터베이스에서 데이터를 즉시 로드하도록 쿼리를 명시적으로 트리거할 수 있습니다. 이는 데이터가 필요할 것을 알고 있고, association 작업 중에 여러 쿼리가 트리거되는 잠재적인 성능 오버헤드를 피하고 싶을 때 유용합니다.
하지만 애플리케이션의 다른 부분에서 데이터가 변경되었을 수 있어서 캐시를 새로고침하고 싶다면 어떻게 할까요? association에서 reload
를 호출하면 됩니다:
# 데이터베이스에서 books를 가져옴
author.books.load
# books의 캐시된 복사본을 사용
author.books.size
# books의 캐시된 복사본을 버리고 데이터베이스에 다시 접근
author.books.reload.empty?
7.2 이름 충돌 피하기
Ruby on Rails 모델에서 association을 생성할 때, ActiveRecord::Base
의 인스턴스 메서드에서 이미 사용 중인 이름을 사용하지 않도록 주의하는 것이 중요합니다.
이는 기존 메서드와 충돌하는 이름으로 association을 생성하면 기본 메서드를 덮어쓰고 기능에 문제를 일으키는 등의 의도하지 않은 결과가 발생할 수 있기 때문입니다. 예를 들어, association에 attributes
나 connection
같은 이름을 사용하는 것은 문제가 될 수 있습니다.
7.3 Schema 업데이트하기
Association은 매우 유용하며 모델 간의 관계를 정의하는 역할을 하지만 데이터베이스 스키마를 업데이트하지는 않습니다. 데이터베이스 스키마를 association과 일치하도록 유지하는 것은 개발자의 책임입니다. 이는 주로 두 가지 주요 작업을 포함합니다: belongs_to
association을 위한 foreign key 생성과 has_many :through
와 has_and_belongs_to_many
association을 위한 올바른 join table 설정입니다. has_many :through vs has_and_belongs_to_many
를 언제 사용해야 하는지에 대해서는 has many through vs has and belongs to many 섹션에서 자세히 읽어볼 수 있습니다.
7.3.1 belongs_to
Association을 위한 Foreign Key 생성하기
belongs_to
association을 선언할 때, 적절한 foreign key를 생성해야 합니다. 예를 들어, 다음 모델을 고려해보세요:
class Book < ApplicationRecord
belongs_to :author
end
이 선언은 books 테이블에서 해당하는 foreign key 컬럼의 지원이 필요합니다. 새로운 테이블의 경우, 마이그레이션은 다음과 같을 수 있습니다:
class CreateBooks < ActiveRecord::Migration[8.1]
def change
create_table :books do |t|
t.datetime :published_at
t.string :book_number
t.belongs_to :author
end
end
end
반면 기존 테이블의 경우, 다음과 같을 수 있습니다:
class AddAuthorToBooks < ActiveRecord::Migration[8.1]
def change
add_reference :books, :author
end
end
7.3.2 has_and_belongs_to_many
Association을 위한 Join Table 생성하기
has_and_belongs_to_many
association을 생성할 때는 join table을 명시적으로 생성해야 합니다. :join_table
옵션을 사용하여 join table의 이름을 명시적으로 지정하지 않는 한, Active Record는 클래스 이름의 사전식 순서를 사용하여 이름을 생성합니다. 따라서 author와 book 모델 간의 join은 "a"가 사전식 순서에서 "b"보다 앞서기 때문에 기본적으로 "authors_books"라는 join table 이름을 갖게 됩니다.
이름이 무엇이든, 적절한 migration으로 join table을 수동으로 생성해야 합니다. 예를 들어, 다음과 같은 association을 고려해보세요:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
assemblies_parts 테이블을 생성하기 위한 migration 작업이 필요합니다.
$ bin/rails generate migration CreateAssembliesPartsJoinTable assemblies parts
그런 다음 마이그레이션을 작성하고 테이블이 primary key 없이 생성되도록 할 수 있습니다.
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[8.1]
def change
create_table :assemblies_parts, id: false do |t|
t.bigint :assembly_id
t.bigint :part_id
end
add_index :assemblies_parts, :assembly_id
add_index :assemblies_parts, :part_id
end
end
join 테이블은 모델을 나타내지 않기 때문에 create_table
에 id: false
를 전달합니다. has_and_belongs_to_many
association에서 손상된 model ID나 충돌하는 ID에 대한 예외와 같은 이상한 동작이 발견된다면, migration을 생성할 때 id: false
를 설정하는 것을 잊었을 가능성이 높습니다.
간단하게 create_join_table
메서드를 사용할 수도 있습니다:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[8.1]
def change
create_join_table :assemblies, :parts do |t|
t.index :assembly_id
t.index :part_id
end
end
end
create_join_table
메서드에 대해서는 Active Record Migration 가이드에서 더 자세히 알아볼 수 있습니다.
7.3.3 has_many :through
Association을 위한 Join 테이블 생성하기
has_many :through
와 has_and_belongs_to_many
의 join 테이블 생성 시 스키마 구현의 주요 차이점은 has_many :through
의 join 테이블에는 id
가 필요하다는 것입니다.
class CreateAppointments < ActiveRecord::Migration[8.1]
def change
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
7.4 Association Scope 제어하기
기본적으로 association은 현재 모듈의 scope 내에서만 객체를 찾습니다. 이 기능은 모듈 내에서 Active Record 모델을 선언할 때 특히 유용하며, association이 적절하게 scope되도록 유지합니다. 예를 들면:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
이 예시에서는 Supplier
와 Account
클래스 모두 동일한 모듈(MyApplication::Business
) 내에 정의되어 있습니다. 이러한 구성을 통해 모든 association에서 scope를 명시적으로 지정할 필요 없이 scope에 따라 모델을 폴더로 구성할 수 있습니다.
# app/models/my_application/business/supplier.rb
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
end
# app/models/my_application/business/account.rb
module MyApplication
module Business
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
모델 스코핑이 코드를 구조화하는데 도움이 되지만, 데이터베이스 테이블의 명명 규칙을 변경하지는 않는다는 점을 기억하는 것이 중요합니다. 예를 들어, MyApplication::Business::Supplier
모델이 있다면, 해당 데이터베이스 테이블은 여전히 명명 규칙을 따라 my_application_business_suppliers
라는 이름을 가져야 합니다.
하지만 Supplier
와 Account
모델이 서로 다른 스코프에 정의되어 있다면, association은 기본적으로 작동하지 않습니다:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
다른 namespace에 있는 모델과 모델을 연결하려면, association 선언에서 완전한 클래스 이름을 지정해야 합니다:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account,
class_name: "MyApplication::Billing::Account"
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier,
class_name: "MyApplication::Business::Supplier"
end
end
end
class_name
옵션을 명시적으로 선언하면 네임스페이스가 다른 모델들 간에도 association을 생성할 수 있습니다. 이를 통해 모듈 스코프와 관계없이 올바른 모델들이 연결되도록 할 수 있습니다.
7.5 양방향 Associations
Rails에서는 모델 간의 associations가 양방향인 것이 일반적입니다. 이는 관련된 두 모델 모두에서 선언되어야 함을 의미합니다. 다음 예시를 살펴보세요:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
Active Record는 연관 이름을 기반으로 이 두 모델이 양방향 연관 관계를 공유한다는 것을 자동으로 식별하려고 시도합니다. 이 정보를 통해 Active Record는 다음과 같은 작업을 할 수 있습니다:
이미 로드된 데이터에 대한 불필요한 쿼리 방지:
Active Record는 이미 로드된 데이터에 대한 추가 데이터베이스 쿼리를 방지합니다.
irb> author = Author.first irb> author.books.all? do |book| irb> book.author.equal?(author) # No additional queries executed here irb> end => true
일관성 없는 데이터 방지:
Author
객체의 복사본이 하나만 로드되므로 일관성 없는 데이터를 방지하는데 도움이 됩니다.irb> author = Author.first irb> book = author.books.first irb> author.name == book.author.name => true irb> author.name = "Changed Name" irb> author.name == book.author.name => true
더 많은 경우에서 연관 관계의 자동 저장:
irb> author = Author.new irb> book = author.books.new irb> book.save! irb> book.persisted? => true irb> author.persisted? => true
더 많은 경우에서 연관 관계의 presence와 absence 검증:
irb> book = Book.new
다음은 한국어 번역입니다:
irb> book.valid?
=> false
irb> book.errors.full_messages
=> ["Author must exist"]
irb> author = Author.new
irb> book = author.books.new
irb> book.valid?
=> true
때로는 :foreign_key
나 :class_name
과 같은 옵션으로 association을 커스터마이징해야 할 수도 있습니다. 이런 경우, Rails는 :through
나 :foreign_key
옵션이 포함된 양방향 association을 자동으로 인식하지 못할 수 있습니다.
반대쪽 association의 커스텀 scope도 자동 식별을 방해하며, config.active_record.automatic_scope_inversing
가 true로 설정되어 있지 않는 한 association 자체의 커스텀 scope도 마찬가지입니다.
예를 들어, 커스텀 foreign key가 있는 다음과 같은 모델 선언을 고려해보세요:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :writer, class_name: "Author", foreign_key: "author_id"
end
:foreign_key
옵션으로 인해 Active Record는 양방향 연관관계를 자동으로 인식하지 못하며, 이는 다음과 같은 여러 문제를 야기할 수 있습니다:
동일한 데이터에 대해 불필요한 쿼리를 실행합니다(이 예시에서는 N+1 쿼리 발생):
irb> author = Author.first irb> author.books.any? do |book| irb> book.writer.equal?(author) # 각 book마다 author 쿼리를 실행합니다 irb> end => false
일관성 없는 데이터를 가진 모델의 여러 복사본을 참조합니다:
irb> author = Author.first irb> book = author.books.first irb> author.name == book.writer.name => true irb> author.name = "Changed Name" irb> author.name == book.writer.name => false
연관관계의 자동 저장이 실패합니다:
irb> author = Author.new irb> book = author.books.new irb> book.save! irb> book.persisted? => true irb> author.persisted? => false
존재 여부 검증이 실패합니다:
irb> author = Author.new irb> book = author.books.new irb> book.valid? => false irb> book.errors.full_messages => ["Author must exist"]
이러한 문제를 해결하기 위해서는 :inverse_of
옵션을 사용하여 명시적으로 양방향 관계를 선언할 수 있습니다:
class Author < ApplicationRecord
has_many :books, inverse_of: "writer"
end
class Book < ApplicationRecord
belongs_to :writer, class_name: "Author", foreign_key: "author_id"
end
위의 예시에서 Book은 belongs_to :writer
를 사용하여 Author를 참조하지만, inverse_of에는 참조할 속성 이름인 "writer"가 지정되어 있습니다.
has_many
연관관계 선언에 :inverse_of
옵션을 포함시키면, Active Record는 양방향 연관관계를 인식하고 위의 초기 예제들에서 설명한 대로 동작할 것입니다.
8 Association References
8.1 옵션
Rails는 대부분의 상황에서 잘 작동하는 지능적인 기본값을 사용하지만, 연관 참조의 동작을 커스터마이즈하고 싶을 때가 있을 수 있습니다. 이러한 커스터마이즈는 연관을 생성할 때 옵션 블록을 전달하여 수행할 수 있습니다. 예를 들어, 다음 연관은 두 가지의 옵션을 사용합니다:
class Book < ApplicationRecord
belongs_to :author, touch: :books_updated_at,
counter_cache: true
end
각 association은 ActiveRecord Associations API의 각 association의 Options
섹션에서 더 자세히 읽을 수 있는 수많은 옵션을 지원합니다. 아래에서 일반적인 사용 사례들에 대해 설명하겠습니다.
8.1.1 :class_name
다른 모델의 이름을 association 이름으로부터 유추할 수 없는 경우, :class_name
옵션을 사용하여 모델 이름을 지정할 수 있습니다. 예를 들어, book이 author에 속하지만 author를 포함하는 실제 모델 이름이 Patron
인 경우 다음과 같이 설정합니다:
class Book < ApplicationRecord
belongs_to :author, class_name: "Patron"
end
8.1.2 :dependent
연관된 객체의 소유자가 삭제될 때 어떤 일이 발생할지를 제어합니다:
:destroy
, 객체가 삭제될 때 연관된 객체들에 대해destroy
가 호출됩니다. 이 메서드는 데이터베이스에서 연관된 레코드를 제거할 뿐만 아니라 정의된 모든 callback들(before_destroy
와after_destroy
같은)이 실행되도록 보장합니다. 이는 로깅이나 관련 데이터 정리와 같은 삭제 프로세스 동안 커스텀 로직을 수행하는 데 유용합니다.:delete
, 객체가 삭제될 때 연관된 모든 객체들이destroy
메서드를 호출하지 않고 데이터베이스에서 직접 삭제됩니다. 이 메서드는 직접적인 삭제를 수행하며 연관된 모델의 callback이나 validation을 건너뛰어 더 효율적이지만, 중요한 정리 작업이 생략되면서 데이터 무결성 문제가 발생할 수 있습니다. 레코드를 빠르게 제거해야 하고 연관된 레코드에 대해 추가 작업이 필요하지 않다고 확신할 때delete
를 사용하세요.:destroy_async
: 객체가 삭제될 때 연관된 객체들에 대해 destroy를 호출하는ActiveRecord::DestroyAssociationAsyncJob
작업이 큐에 추가됩니다. 이것이 작동하려면 Active Job이 설정되어 있어야 합니다. 데이터베이스에서 foreign key 제약조건으로 관리되는 association에는 이 옵션을 사용하지 마세요. foreign key 제약조건 작업은 소유자를 삭제하는 동일한 트랜잭션 내에서 발생합니다.:nullify
는 foreign key를NULL
로 설정하게 합니다. polymorphic association에서는 polymorphic type 컬럼도 null로 설정됩니다. callback은 실행되지 않습니다.:restrict_with_exception
는 연관된 레코드가 있을 경우ActiveRecord::DeleteRestrictionError
예외를 발생시킵니다.:restrict_with_error
는 연관된 객체가 있을 경우 소유자에게 에러를 추가합니다.
다른 클래스의 has_many
association과 연결된 belongs_to
association에서는 이 옵션을 지정하면 안 됩니다. 그렇게 하면 부모 객체를 삭제할 때 자식들을 삭제하려 시도하고, 이는 다시 부모를 삭제하려 시도할 수 있어 데이터베이스에 고아 레코드가 생길 수 있습니다.
NOT NULL
데이터베이스 제약조건이 있는 association에 대해 :nullify
옵션을 남겨두지 마세요. :dependent
를 :destroy
로 설정하는 것이 필수적입니다. 그렇지 않으면 연관된 객체의 foreign key가 NULL
로 설정되어 변경이 불가능할 수 있습니다.
:dependent
옵션은 :through
옵션과 함께 사용될 때 무시됩니다. :through
를 사용할 때는 join 모델이 belongs_to
association을 가져야 하며, 삭제는 연관된 레코드가 아닌 join 레코드에만 영향을 미칩니다.
scoped association에서 dependent: :destroy
를 사용할 때는 scoped된 객체들만 삭제됩니다. 예를 들어, Post
모델에서 has_many :comments, -> { where published: true }, dependent: :destroy
로 정의된 경우, destroy를 호출하면
post에서는 published comment들만 삭제되며, 삭제된 post를 가리키는 foreign key가 있는 unpublished comment들은 그대로 유지됩니다.
has_and_belongs_to_many
association에서는 :dependent
option을 직접 사용할 수 없습니다. join table 레코드의 삭제를 관리하려면, 수동으로 처리하거나 더 많은 유연성을 제공하고 :dependent
option을 지원하는 has_many :through
association으로 전환하세요.
8.1.3 :foreign_key
관례적으로, Rails는 이 model에서 foreign key를 저장하는데 사용되는 column이 association의 이름에 _id
접미사가 추가된 것이라고 가정합니다. :foreign_key
option을 사용하면 foreign key의 이름을 직접 설정할 수 있습니다:
class Supplier < ApplicationRecord
has_one :account, foreign_key: "supp_id"
end
주의: Rails는 foreign key 컬럼을 자동으로 생성하지 않습니다. migration에서 명시적으로 정의해야 합니다.
8.1.4 :primary_key
기본적으로 Rails는 테이블의 primary key로 id
컬럼을 사용합니다. :primary_key
옵션을 사용하면 다른 컬럼을 primary key로 지정할 수 있습니다.
예를 들어, users
테이블이 id
대신 guid
를 primary key로 사용하고, todos
테이블에서 guid
를 foreign key(user_id
)로 참조하고 싶다면 다음과 같이 설정할 수 있습니다:
class User < ApplicationRecord
self.primary_key = "guid" # id 대신 guid를 기본키로 설정
end
class Todo < ApplicationRecord
belongs_to :user, primary_key: "guid" # users 테이블의 guid 컬럼을 참조
end
@user.todos.create
를 실행하면 @todo
레코드의 user_id
값이 @user
의 guid
값으로 설정됩니다.
has_and_belongs_to_many
는 :primary_key
옵션을 지원하지 않습니다. 이런 종류의 association에서는 has_many :through
association을 사용하는 join 테이블을 통해 비슷한 기능을 구현할 수 있으며, 이는 더 많은 유연성을 제공하고 :primary_key
옵션을 지원합니다. 이에 대해 더 자세히 알아보려면 has_many :through
섹션을 참조하세요.
8.1.5 :touch
:touch
옵션을 true
로 설정하면, 이 객체가 저장되거나 삭제될 때마다 연관된 객체의 updated_at
또는 updated_on
타임스탬프가 현재 시간으로 설정됩니다:
class Book < ApplicationRecord
belongs_to :author, touch: true
end
class Author < ApplicationRecord
has_many :books
end
위의 코드에서 Book이 업데이트될 때마다 관련된 author의 updated_at
/updated_on
timestamp도 함께 업데이트됩니다.
이 경우에는 book을 저장하거나 삭제하면 관련된 author의 timestamp가 업데이트됩니다. 또한 업데이트할 특정 timestamp 속성을 지정할 수도 있습니다:
class Book < ApplicationRecord
belongs_to :author, touch: :books_updated_at
end
belongs_to
의 :touch
옵션이 column 이름으로 설정될 때, 이 association이 touch되면 해당 column이 현재 timestamp로 업데이트됩니다. 이 경우에는 관련된 author
record의 books_updated_at
column이 업데이트됩니다.
has_and_belongs_to_many
는 :touch
옵션을 지원하지 않습니다. 이러한 타입의 association에서는 has_many :through
association과 조인 테이블을 사용하여 비슷한 기능을 구현할 수 있습니다. 이에 대해서는 has_many :through
섹션에서 더 자세히 읽어볼 수 있습니다.
8.1.6 :validate
:validate
옵션을 true
로 설정하면, 이 객체를 저장할 때마다 새로운 관련 객체들이 유효성 검증됩니다. 기본값은 false
입니다: 이 객체가 저장될 때 새로운 관련 객체들은 유효성 검증되지 않습니다.
has_and_belongs_to_many
는 :validate
옵션을 지원하지 않습니다. 이러한 타입의 association에서는 has_many :through
association과 조인 테이블을 사용하여 비슷한 기능을 구현할 수 있습니다. 이에 대해서는 has_many :through
섹션에서 더 자세히 읽어볼 수 있습니다.
8.1.7 :inverse_of
:inverse_of
옵션은 이 association의 역방향인 belongs_to
association의 이름을 지정합니다. 자세한 내용은 양방향 association 섹션을 참조하세요.
class Supplier < ApplicationRecord
has_one :account, inverse_of: :supplier
end
class Account < ApplicationRecord
belongs_to :supplier, inverse_of: :account
end
위와 같이 inverse_of
를 명시적으로 선언하면, Active Record는 양방향 관계에서 서로의 인스턴스가 동일한 것임을 알 수 있게 됩니다.
8.1.8 :source_type
:source_type
옵션은 polymorphic association을 통과하는 has_many :through
association의 source association 타입을 지정합니다.
class Author < ApplicationRecord
has_many :books
has_many :paperbacks, through: :books, source: :format, source_type: "Paperback"
end
class Book < ApplicationRecord
belongs_to :format, polymorphic: true
end
class Hardback < ApplicationRecord; end
class Paperback < ApplicationRecord; end
8.1.9 :strict_loading
이 association을 통해 관련 레코드가 로드될 때마다 strict loading을 강제합니다.
8.1.10 :association_foreign_key
:association_foreign_key
는 has_and_belongs_to_many
관계에서 찾을 수 있습니다. 규칙에 따라, Rails는 join 테이블에서 다른 모델을 가리키는 foreign key를 포함하는 컬럼이 해당 모델의 이름에 _id
접미사가 추가된 것이라고 가정합니다. :association_foreign_key
옵션을 사용하면 foreign key의 이름을 직접 설정할 수 있습니다. 예를 들어:
class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User", # 관계가 설정될 클래스 이름
foreign_key: "this_user_id", # 자신을 가리키는 외래키
association_foreign_key: "other_user_id" # 상대방을 가리키는 외래키
end
:foreign_key
와 :association_foreign_key
옵션은 many-to-many self-join을 설정할 때 유용합니다.
8.1.11 :join_table
:join_table
은 has_and_belongs_to_many
관계에서 찾을 수 있습니다. 사전식 순서에 기반한 join table의 기본 이름이 원하는 것이 아닐 경우, :join_table
옵션을 사용하여 기본값을 재정의할 수 있습니다.
8.2 Scopes
Scope는 association 객체에서 메서드 호출로 참조할 수 있는 공통 쿼리를 지정할 수 있게 해줍니다. 이는 애플리케이션의 여러 곳에서 재사용되는 커스텀 쿼리를 정의하는데 유용합니다. 예를 들어:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { where active: true }
end
만약 Parts model이 active 상태인 assemblies와만 관계를 맺도록 하고 싶다면, 이렇게 조건을 추가할 수 있습니다.
8.2.1 일반적인 Scope들
scope 블럭 안에서 표준 querying methods를 사용할 수 있습니다. 아래에서 다음 메서드들을 살펴보겠습니다:
where
includes
readonly
select
8.2.1.1 where
where
메서드는 관련 객체가 충족해야 하는 조건을 지정할 수 있게 해줍니다.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where "factory = 'Seattle'" }
end
해시를 통해서도 조건을 설정할 수 있습니다:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where factory: "Seattle" }
# 조건: factory가 "Seattle"인 assemblies와만 관계를 형성
end
만약 hash 스타일의 where
를 사용하면, 이 association을 통한 레코드 생성은 자동으로 해당 hash로 scope가 지정됩니다. 이 경우, @parts.assemblies.create
또는 @parts.assemblies.build
를 사용하면 factory
컬럼의 값이 "Seattle"인 assemblies가 생성됩니다.
8.2.1.2 includes
includes
메서드를 사용하여 이 association이 사용될 때 eager-load 되어야 하는 2차 association을 지정할 수 있습니다. 예를 들어, 다음과 같은 모델들을 고려해보세요:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
class Representative < ApplicationRecord
has_many :accounts
end
만약 supplier로부터 representative를 직접 자주 조회한다면(@supplier.account.representative
), supplier와 account 간의 association에 representative를 포함시켜 코드를 더 효율적으로 만들 수 있습니다:
class Supplier < ApplicationRecord
has_one :account, -> { includes :representative }
end
class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
class Representative < ApplicationRecord
has_many :accounts
end
즉각적인 관계(immediate associations)에서는 includes
를 사용할 필요가 없습니다. 다시 말해, Book belongs_to :author
와 같은 관계를 가지고 있다면 author는 필요할 때 자동으로 eager-load 됩니다.
8.2.1.3 readonly
readonly
를 사용하면, association을 통해 검색된 관련 객체는 읽기 전용이 됩니다.
class Book < ApplicationRecord
belongs_to :author, -> { readonly }
end
Author record는 read-only로 설정되어 수정할 수 없습니다. Author object에서 속성을 변경하고 save
를 호출하면 ActiveRecord::ReadOnlyRecord
가 발생합니다.
연관된 객체가 association을 통해 수정되는 것을 방지하고 싶을 때 유용합니다. 예를 들어, belongs_to :author
인 Book
모델이 있다면, readonly
를 사용하여 book을 통해 author가 수정되는 것을 방지할 수 있습니다:
@book.author = Author.first
@book.author.save! # 이는 ActiveRecord::ReadOnlyRecord 에러를 발생시킬 것입니다
8.2.1.4 select
select
메소드는 관련 객체의 데이터를 검색하는데 사용되는 SQL SELECT
절을 재정의할 수 있게 합니다. 기본적으로 Rails는 모든 컬럼을 검색합니다.
예를 들어, 여러 Book
을 가진 Author
모델이 있지만 각 book의 title
만 검색하고 싶은 경우:
class Author < ApplicationRecord
has_many :books, -> { select(:id, :title) } # id와 title 컬럼만 선택
end
class Book < ApplicationRecord
belongs_to :author
end
이제 author의 books에 접근할 때 books
테이블에서 id
와 title
컬럼만 검색됩니다.
belongs_to
연관관계에서 select
메서드를 사용할 경우, 올바른 결과를 보장하기 위해 :foreign_key
옵션도 함께 설정해야 합니다. 예를 들어:
class Book < ApplicationRecord
belongs_to :author, -> { select(:id, :name) }, foreign_key: "author_id" # id와 name 컬럼만 select
end
class Author < ApplicationRecord
has_many :books
end
이 경우, book의 author에 접근할 때 authors
테이블에서 id
와 name
컬럼만 조회됩니다.
8.2.2 Collection Scopes
has_many
와 has_and_belongs_to_many
는 레코드의 컬렉션을 다루는 association이므로, group
, limit
, order
, select
, distinct
와 같은 추가 메서드를 사용하여 association이 사용하는 쿼리를 커스터마이즈할 수 있습니다.
8.2.2.1 group
group
메서드는 결과 세트를 그룹화할 속성 이름을 제공하며, finder SQL에서 GROUP BY
절을 사용합니다.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { group "factory" }
end
8.2.2.2 limit
limit
메서드는 association을 통해 가져올 총 객체 수를 제한할 수 있게 해줍니다.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order("created_at DESC").limit(50) }
end
8.2.2.3 order
order
메서드는 관련된 객체들이 받아지는 순서를 지정합니다(SQL의 ORDER BY
절에서 사용되는 문법으로).
class Author < ApplicationRecord
has_many :books, -> { order "date_confirmed DESC" }
# Author는 많은 books를 가지며, date_confirmed의 내림차순으로 정렬됩니다
end
8.2.2.4 select
select
메서드를 사용하면 관련된 객체들의 데이터를 조회할 때 사용되는 SQL SELECT
절을 재정의할 수 있습니다. 기본적으로 Rails는 모든 컬럼을 조회합니다.
직접 select
를 지정할 경우, 반드시 관련 모델의 primary key와 foreign key 컬럼을 포함해야 합니다. 그렇지 않으면 Rails가 에러를 발생시킵니다.
8.2.2.5 distinct
distinct
메서드를 사용하면 컬렉션에서 중복을 제거할 수 있습니다. 이는 주로 :through
옵션과 함께 사용할 때 유용합니다.
class Person < ApplicationRecord
has_many :readings
has_many :articles, through: :readings
end
irb> person = Person.create(name: 'John')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
irb> person.articles.to_a
=> [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
irb> Reading.all.to_a
=> [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
위의 경우 두 개의 readings가 있고 이러한 레코드들이 같은 article을 가리키고 있음에도 불구하고 person.articles
는 두 개 모두를 가져옵니다.
이제 distinct
를 설정해보겠습니다:
class Person
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
irb> person.articles.to_a
=> [#<Article id: 7, name: "a1">]
irb> Reading.all.to_a
=> [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
위의 경우에도 두 개의 readings가 있습니다. 하지만 person.articles
는 collection이 고유한 records만 로드하기 때문에 하나의 article만 표시됩니다.
삽입 시 persist된 association의 모든 records가 고유하도록 보장하고 싶다면(이를 통해 association을 검사할 때 중복 records를 발견하지 않도록), 테이블 자체에 unique index를 추가해야 합니다. 예를 들어, readings
라는 테이블이 있고 articles가 한 person에 한 번만 추가되도록 보장하고 싶다면, migration에 다음과 같이 추가할 수 있습니다:
add_index :readings, [:person_id, :article_id], unique: true
person_id와 article_id가 결합된 유니크 인덱스를 readings 테이블에 추가합니다.
이 unique index가 설정되면, article을 한 사람에게 두 번 추가하려고 할 때 ActiveRecord::RecordNotUnique
에러가 발생합니다:
irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
ActiveRecord::RecordNotUnique
위의 예제에서는 같은 article을 두 번 추가하려고 하기 때문에 ActiveRecord::RecordNotUnique 에러가 발생합니다.
include?
와 같은 것을 사용하여 유일성을 확인하는 것은 race condition의 영향을 받습니다. association에서 고유성을 강제하기 위해 include?
를 사용하지 마세요. 예를 들어, 위의 article 예제를 사용하여 다음과 같은 코드를 작성하면 여러 사용자가 동시에 시도할 수 있기 때문에 race condition이 발생할 수 있습니다:
person.articles에 해당 article이 없는 경우에만 person.articles에 article을 추가
8.2.3 Association 소유자 사용하기
Association 스코프를 더 세밀하게 제어하기 위해 scope 블록에 단일 인수로 association의 소유자를 전달할 수 있습니다. 하지만 이렇게 하면 association의 preloading이 불가능해진다는 점에 유의하세요.
예시:
class Supplier < ApplicationRecord
has_one :account, ->(supplier) { where active: supplier.active? }
end
이 예시에서 Supplier
모델의 account
association은 supplier의 active
상태를 기반으로 스코프가 지정됩니다.
association 확장과 association 소유자를 통한 스코핑을 활용함으로써, Rails 애플리케이션에서 더욱 동적이고 컨텍스트를 인식하는 association을 만들 수 있습니다.
8.3 Counter Cache
Rails의 :counter_cache
옵션은 연관된 객체의 수를 찾는 효율성을 향상시키는데 도움을 줍니다. 다음의 모델들을 살펴보세요:
class Book < ApplicationRecord
belongs_to :author
end
class Author < ApplicationRecord
has_many :books
end
기본적으로 @auth books.size
를 쿼리하면 COUNT(*)
쿼리를 수행하기 위해 database 호출이 발생합니다. 이를 최적화하기 위해 belonging 모델(이 경우 Book
)에 counter cache를 추가할 수 있습니다. 이렇게 하면 Rails는 database를 쿼리하지 않고 cache에서 직접 count를 반환할 수 있습니다.
class Book < ApplicationRecord
belongs_to :author, counter_cache: true
end
class Author < ApplicationRecord
has_many :books
end
이 선언으로 Rails는 cache 값을 최신 상태로 유지하고, size
메소드에 대한 응답으로 해당 값을 반환하여 데이터베이스 호출을 피할 수 있습니다.
:counter_cache
옵션이 belongs_to
선언이 있는 모델에 지정되어 있지만, 실제 컬럼은 연관된 (이 경우 has_many
) 모델에 추가되어야 합니다. 이 예시에서는 Author
모델에 books_count
컬럼을 추가해야 합니다:
class AddBooksCountToAuthors < ActiveRecord::Migration[8.1]
def change
add_column :authors, :books_count, :integer, default: 0, null: false
end
end
counter_cache
선언에서 기본값인 books_count
대신 사용자 정의 컬럼명을 지정할 수 있습니다. 예를 들어, count_of_books
를 사용하려면:
class Book < ApplicationRecord
belongs_to :author, counter_cache: :count_of_books
end
class Author < ApplicationRecord
has_many :books
end
:counter_cache
옵션은 association의 belongs_to
측에만 지정하면 됩니다.
기존의 대규모 테이블에서 counter cache를 사용하는 것은 까다로울 수 있습니다. 테이블이 너무 오랫동안 잠기는 것을 피하기 위해, 컬럼 값들은 컬럼 추가와는 별도로 채워져야 합니다. 이 백필(backfill)은 :counter_cache
사용 전에 이루어져야 합니다. 그렇지 않으면 counter cache에 의존하는 size
, any?
등과 같은 메서드들이 잘못된 결과를 반환할 수 있습니다.
자식 레코드 생성/제거와 함께 counter cache 컬럼을 안전하게 업데이트하면서 값을 안전하게 백필하고, 메서드들이 항상 데이터베이스에서 결과를 가져오도록 하기 위해서는(초기화되지 않은 counter cache의 잠재적으로 잘못된 값을 피하기 위해) counter_cache: { active: false }
를 사용하세요. 이 설정은 메서드들이 항상 데이터베이스에서 결과를 가져오도록 보장하여, 초기화되지 않은 counter cache의 잘못된 값을 피할 수 있습니다. 만약 사용자 정의 컬럼 이름을 지정해야 한다면, counter_cache: { active: false, column: :my_custom_counter }
를 사용하세요.
어떤 이유로든 owner 모델의 primary key 값을 변경하고, 카운트된 모델들의 foreign key도 업데이트하지 않으면, counter cache가 오래된 데이터를 가질 수 있습니다. 다시 말해, 고아가 된 모델들도 여전히 카운터에 포함됩니다. 오래된 counter cache를 수정하려면 reset_counters
를 사용하세요.
8.4 Callbacks
일반적인 callback은 Active Record 객체의 생명주기에 연결되어, 다양한 시점에서 해당 객체들과 작업할 수 있게 해줍니다. 예를 들어, :before_save
callback을 사용하여 객체가 저장되기 직전에 특정 작업이 발생하도록 할 수 있습니다.
Association callback은 일반적인 callback과 유사하지만, Active Record 객체와 연관된 collection의 생명주기 이벤트에 의해 트리거됩니다. 네 가지 association callback이 있습니다:
before_add
after_add
before_remove
after_remove
Association callback은 association 선언에 옵션을 추가하여 정의합니다. 예를 들면:
class Author < ApplicationRecord
has_many :books, before_add: :check_credit_limit
def check_credit_limit(book)
throw(:abort) if limit_reached?
end
end
이 예시는 한도 초과를 방지하기 위해 Book이 Author와 연결되기 전에 검사를 수행합니다.
이 예시에서 Author
모델은 books
와 has_many
관계를 가집니다. before_add
콜백인 check_credit_limit
는 컬렉션에 book이 추가되기 전에 트리거됩니다. 만약 limit_reached?
메서드가 true
를 반환하면 book은 컬렉션에 추가되지 않습니다.
이러한 association 콜백들을 사용함으로써 컬렉션 생명주기의 주요 시점에서 특정 동작이 수행되도록 하여 association의 동작을 커스터마이즈할 수 있습니다.
association 콜백에 대해 더 자세히 알아보시려면 Active Record Callbacks 가이드를 참고하세요.
8.5 Extensions
Rails는 association proxy 객체(association들을 관리하는)의 기능을 확장할 수 있는 기능을 제공합니다. 익명 모듈을 통해 새로운 finder, creator 또는 다른 메소드를 추가할 수 있습니다. 이 기능을 통해 애플리케이션의 특정 요구사항에 맞게 association을 커스터마이즈할 수 있습니다.
모델 정의 내에서 직접 has_many
association에 커스텀 메소드를 확장할 수 있습니다. 예를 들면:
class Author < ApplicationRecord
has_many :books do
def find_by_book_prefix(book_number)
find_by(category_id: book_number[0..2])
end
end
end
이 예제에서는 find_by_book_prefix
메소드가 Author
모델의 books
association에 추가됩니다. 이 커스텀 메소드를 사용하면 book_number
의 특정 접두사를 기반으로 books
를 찾을 수 있습니다.
여러 association에서 공유해야 하는 extension이 있는 경우, named extension module을 사용할 수 있습니다. 예를 들어:
module FindRecentExtension
def find_recent
where("created_at > ?", 5.days.ago)
end
end
class Author < ApplicationRecord
has_many :books, -> { extending FindRecentExtension }
end
class Supplier < ApplicationRecord
has_many :deliveries, -> { extending FindRecentExtension }
end
위 예시에서는 여러 다른 모델의 관계에서 재사용할 수 있는 공통 기능을 정의하고 있습니다. 각 모델 클래스는 FindRecentExtension
module을 extend하여 find_recent
라는 메소드를 획득합니다.
이 경우, FindRecentExtension
모듈은 Author
모델의 books
association과 Supplier
모델의 deliveries
association 모두에 find_recent
메서드를 추가하는 데 사용됩니다. 이 메서드는 최근 5일 이내에 생성된 레코드들을 검색합니다.
Extension은 proxy_association
접근자를 사용하여 association 프록시의 내부와 상호작용할 수 있습니다. proxy_association
은 세 가지 중요한 속성을 제공합니다:
proxy_association.owner
는 association이 속한 객체를 반환합니다.proxy_association.reflection
은 association을 설명하는 reflection 객체를 반환합니다.proxy_association.target
은belongs_to
나has_one
의 경우 연관된 객체를,has_many
나has_and_belongs_to_many
의 경우 연관된 객체들의 컬렉션을 반환합니다.
이러한 속성들을 통해 extension은 association 프록시의 내부 상태와 동작에 접근하고 조작할 수 있습니다.
다음은 extension에서 이러한 속성들을 사용하는 방법을 보여주는 고급 예제입니다:
module AdvancedExtension
def find_and_log(query)
results = where(query)
proxy_association.owner.logger.info("#{proxy_association.reflection.name}을(를) #{query}로 쿼리하는 중")
results
end
end
class Author < ApplicationRecord
has_many :books, -> { extending AdvancedExtension }
end
이 예제에서 find_and_log
method는 association에 대한 query를 수행하고 owner의 logger를 사용하여 query 세부사항을 기록합니다. 이 method는 proxy_association.owner
를 통해 owner의 logger에 접근하고 proxy_association.reflection.name
을 통해 association의 이름에 접근합니다.