rubyonrails.org에서 더 보기:

GitHub에서 이 파일을 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Active Record Associations

이 가이드는 Active Record의 association 기능을 다룹니다.

이 가이드를 읽고 나면 다음과 같은 내용을 알 수 있습니다:

  • 다양한 종류의 associations를 이해하기
  • Active Record 모델 간의 associations 선언하기
  • 모델에 맞는 올바른 association 타입 선택하기
  • Single Table Inheritance 사용하기
  • Delegated Types 설정 및 사용하기

1 Associations 개요

Active Record associations를 사용하면 모델 간의 관계를 정의할 수 있습니다. Associations는 특별한 매크로 스타일 호출로 구현되어 Rails에게 모델들이 서로 어떻게 연관되어 있는지 쉽게 알려줄 수 있으며, 이는 데이터를 더 효과적으로 관리하고 일반적인 작업을 더 단순하고 읽기 쉽게 만드는 데 도움이 됩니다.

매크로 스타일 호출은 런타임에 다른 메서드를 생성하거나 수정하는 메서드로, Rails에서 모델 associations를 정의하는 것과 같이 기능을 간결하고 표현력 있게 선언할 수 있게 합니다. 예를 들어, has_many :comments와 같습니다.

association을 설정하면 Rails는 두 모델 인스턴스 간의 Primary KeyForeign 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 Association Diagram

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: trueauthor_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에서 확인할 수 있습니다.

위의 모든 메서드에서, associationbelongs_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_manyhas_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_onebelongs_to와의 주요 차이점은 링크 컬럼(이 경우 supplier_id)이 has_one이 선언된 테이블이 아닌 다른 테이블에 위치한다는 것입니다.

has_one Association Diagram

해당하는 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_idsuppliers 테이블의 유효한 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 참조와 마찬가지로, 이러한 모든 메서드에서 associationhas_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_manyhas_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_manyhas_one과 유사하지만 다른 모델과의 일대다 관계를 나타냅니다. 이 association은 주로 belongs_to association의 "반대쪽"에서 발견됩니다. 이 association은 모델의 각 인스턴스가 다른 모델의 0개 이상의 인스턴스를 가질 수 있음을 나타냅니다. 예를 들어, author와 book이 있는 애플리케이션에서 author 모델은 다음과 같이 선언될 수 있습니다:

class Author < ApplicationRecord
  has_many :books
end

has_many는 모델 간의 일대다 관계를 설정하여, 선언하는 모델(Author)의 각 인스턴스가 연관된 모델(Book)의 여러 인스턴스를 가질 수 있도록 합니다.

has_onebelongs_to 연관관계와 달리, has_many 연관관계를 선언할 때는 다른 모델의 이름이 복수형이 됩니다.

has_many Association Diagram

해당하는 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은 booksauthors 테이블 간의 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 관계를 선언하면, 선언하는 클래스는 관계와 관련된 많은 메서드들을 얻게 됩니다. 그 중 일부는 다음과 같습니다:

일반적인 메서드들 중 일부를 살펴보겠지만, 전체 목록은 ActiveRecord Associations API에서 찾을 수 있습니다.

이 모든 메서드에서 collectionhas_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)의 여러 인스턴스와 연결될 수 있도록 합니다.

has_many :through Association
Diagram

해당하는 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 컬럼을 가진 physicianspatients 테이블이 생성됩니다. join 테이블 역할을 하는 appointments 테이블은 physician_idpatient_id 컬럼으로 생성되어 physicianspatients 간의 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에 접근할 수 있게 해줍니다.

has_one :through Association
Diagram

이러한 association을 설정하기 위한 migration은 다음과 같을 수 있습니다:

다음 migration은 3개의 테이블을 생성합니다:

  1. suppliers 테이블은 name 컬럼을 가집니다.
  2. accounts 테이블은 supplier와 belongs_to 관계를 가지며, account_number 컬럼을 가집니다.
  3. 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개 이상의 인스턴스를 참조할 수 있음을 나타냅니다.

예를 들어, AssemblyPart 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 Association
Diagram

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과 관련된 다수의 메서드를 얻게 됩니다. 그 중 일부는 다음과 같습니다:

일반적인 메서드들을 살펴보겠지만, 전체 목록은 ActiveRecord Associations API에서 확인할 수 있습니다.

이 모든 메서드에서 collectionhas_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.concatcollection.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_tohas_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 :throughhas_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

Polymorphic Association Diagram

위 컨텍스트에서 imageable은 association을 위해 선택된 이름입니다. 이는 Picture 모델과 EmployeeProduct와 같은 다른 모델들 간의 polymorphic association을 나타내는 상징적인 이름입니다. 중요한 점은 polymorphic association을 올바르게 설정하기 위해 모든 관련 모델에서 동일한 이름(imageable)을 일관되게 사용하는 것입니다.

Picture 모델에서 belongs_to :imageable, polymorphic: true를 선언할 때, Picture가 이 association을 통해 어떤 모델(예: EmployeeProduct)에도 속할 수 있다고 명시하는 것입니다.

polymorphic belongs_to 선언은 다른 모델이 사용할 수 있는 인터페이스를 설정하는 것으로 생각할 수 있습니다. 이를 통해 Employee 모델의 인스턴스에서 @employee.pictures를 사용하여 사진 컬렉션을 가져올 수 있습니다. 마찬가지로 Product 모델의 인스턴스에서 @product.pictures를 사용하여 사진 컬렉션을 가져올 수 있습니다.

또한 Picture 모델의 인스턴스가 있다면, @picture.imageable을 통해 그 상위 객체를 가져올 수 있으며, 이는 EmployeeProduct가 될 수 있습니다.

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_idEmployee 또는 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 :manageremployees 테이블에 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 모델이 있다고 가정해봅시다. 이 모델들은 colorprice 같은 필드를 공유하지만, 각각 고유한 동작을 가집니다. 또한 각각 자신만의 컨트롤러를 가지게 됩니다.

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"로 저장됩니다.

MotorcycleBicycle에도 동일한 과정을 반복하세요.

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_columnnil로 설정하면 됩니다.

# 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을 위해서 새로운 MessageComment 모델을 생성할 것입니다:

$ 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 선언하기

먼저, 슈퍼클래스 Entrydelegated_type을 선언합니다.

class Entry < ApplicationRecord
  delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
end

entryable 파라미터는 위임에 사용할 필드를 지정하며, MessageComment를 위임 클래스로 포함합니다. entryable_typeentryable_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를 향상시킬 수 있습니다. 예를 들어 Entrytitle 메서드를 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 메서드를 위임할 수 있게 합니다. 여기서 Messagesubject를 사용하고 Commentcontent의 잘린 버전을 사용합니다.

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에 attributesconnection 같은 이름을 사용하는 것은 문제가 될 수 있습니다.

7.3 Schema 업데이트하기

Association은 매우 유용하며 모델 간의 관계를 정의하는 역할을 하지만 데이터베이스 스키마를 업데이트하지는 않습니다. 데이터베이스 스키마를 association과 일치하도록 유지하는 것은 개발자의 책임입니다. 이는 주로 두 가지 주요 작업을 포함합니다: belongs_to association을 위한 foreign key 생성과 has_many :throughhas_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_tableid: 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 :throughhas_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

이 예시에서는 SupplierAccount 클래스 모두 동일한 모듈(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라는 이름을 가져야 합니다.

하지만 SupplierAccount 모델이 서로 다른 스코프에 정의되어 있다면, 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
    
  • 더 많은 경우에서 연관 관계의 presenceabsence 검증:

    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_destroyafter_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 값이 @userguid 값으로 설정됩니다.

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_keyhas_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_tablehas_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 :authorBook 모델이 있다면, 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 테이블에서 idtitle 컬럼만 검색됩니다.

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 테이블에서 idname 컬럼만 조회됩니다.

8.2.2 Collection Scopes

has_manyhas_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 모델은 bookshas_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.targetbelongs_tohas_one의 경우 연관된 객체를, has_manyhas_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의 이름에 접근합니다.



맨 위로