액티브 레코드 연관관계

이 가이드는 액티브 레코드의 연관관계 기능을 다룹니다.

이 가이드를 읽고 나면 다음을 알 수 있습니다:

  • 액티브 레코드 모델 간의 연관관계를 선언하는 방법
  • 다양한 유형의 액티브 레코드 연관관계를 이해하는 방법
  • 연관관계 생성으로 인해 모델에 추가되는 메서드를 사용하는 방법

연관관계의 필요성

Rails에서 연관관계는 두 액티브 레코드 모델 간의 연결을 의미합니다. 모델 간 연관관계가 왜 필요할까요? 연관관계는 코드에서 일반적인 작업을 더 간단하고 쉽게 만들어주기 때문입니다.

예를 들어, 저자와 책 모델이 있는 간단한 Rails 애플리케이션을 생각해 보겠습니다. 각 저자는 여러 권의 책을 가질 수 있습니다.

연관관계 없이 모델 선언은 다음과 같이 보일 것입니다:

class Author < ApplicationRecord
end

class Book < ApplicationRecord
end

이제 기존 저자에게 새로운 책을 추가하고 싶다고 가정해 보겠습니다. 다음과 같이 해야 할 것입니다:

@book = Book.create(published_at: Time.now, author_id: @author.id)

또는 저자를 삭제하고 그 저자의 모든 책도 삭제하고 싶다고 가정해 보겠습니다:

@books = Book.where(author_id: @author.id)
@books.each do |book|
  book.destroy
end
@author.destroy

액티브 레코드 연관관계를 사용하면 이러한 작업을 간소화할 수 있습니다. 저자와 책 간의 연결을 선언적으로 알려주면 Rails가 이를 처리할 수 있습니다. 다음과 같이 저자와 책을 설정할 수 있습니다:

class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end

class Book < ApplicationRecord
  belongs_to :author
end

이렇게 변경하면 특정 저자에게 새로운 책을 추가하는 것이 더 쉬워집니다:

@book = @author.books.create(published_at: Time.now)

저자를 삭제하고 그 저자의 모든 책을 삭제하는 것도 훨씬 쉬워집니다:

@author.destroy

다음 섹션에서 다양한 유형의 연관관계에 대해 자세히 알아보겠습니다. 그 다음에는 연관관계 사용에 대한 팁과 요령, 그리고 Rails의 연관관계에 대한 전체 참조 자료를 다루겠습니다.

연관관계의 유형

Rails는 각각 특정한 사용 사례를 가진 6가지 유형의 연관관계를 지원합니다.

다음은 모든 지원되는 유형의 목록과 각각에 대한 API 문서에 대한 링크입니다. 이 문서에서는 사용 방법, 메서드 매개변수 등에 대한 자세한 정보를 제공합니다.

연관관계는 매크로 스타일 호출을 사용하여 구현되므로, 모델에 기능을 선언적으로 추가할 수 있습니다. 예를 들어, 한 모델이 belongs_to 다른 모델을 선언하면, Rails는 두 모델 간의 기본 키-외래 키 정보를 유지관리하고, 모델에 많은 유틸리티 메서드를 추가합니다.

이 가이드의 나머지 부분에서는 다양한 형태의 연관관계를 선언하고 사용하는 방법을 배우게 됩니다. 하지만 먼저 각 연관관계 유형이 적합한 상황에 대한 간단한 소개를 하겠습니다.

belongs_to 연관관계

belongs_to 연관관계는 다른 모델과의 연결을 설정합니다. 이 연관관계는 선언 모델의 각 인스턴스가 다른 모델의 한 인스턴스에 “속한다"는 것을 의미합니다. 예를 들어, 애플리케이션에 저자와 책이 있고, 각 책은 정확히 한 명의 저자에게 할당될 수 있다면, 책 모델을 다음과 같이 선언할 수 있습니다:

class Book < ApplicationRecord
  belongs_to :author
end

belongs_to 연관관계 다이어그램

참고: belongs_to 연관관계는 반드시 단수 용어를 사용해야 합니다. 위의 예제에서 Book 모델의 author 연관관계를 복수형으로 사용하고 Book.create(authors: @author)와 같이 인스턴스를 생성하려고 하면 "uninitialized constant Book::Authors"라는 오류가 발생할 것입니다. 이는 Rails가 연관관계 이름에서 클래스 이름을 자동으로 유추하기 때문입니다. 연관관계 이름이 잘못 복수형으로 지정되면 유추된 클래스도 잘못 복수형이 됩니다.

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateBooks < ActiveRecord::Migration[7.2]
  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는 단방향 일대일 연결을 생성합니다. 따라서 위의 예제에서 각 책은 저자를 "알고” 있지만, 저자는 자신의 책을 모릅니다. 양방향 연관관계를 설정하려면 belongs_to를 다른 모델(이 경우 Author 모델)의 has_one 또는 has_many와 함께 사용해야 합니다.

belongs_tooptional이 true로 설정된 경우 참조 일관성을 보장하지 않으므로, 사용 사례에 따라 참조 열에 데이터베이스 수준의 외래 키 제약 조건을 추가해야 할 수 있습니다. 예를 들면 다음과 같습니다:

create_table :books do |t|
  t.belongs_to :author, foreign_key: true
  # ...
end

has_one 연관관계

has_one 연관관계는 다른 모델이 이 모델에 대한 참조를 가지고 있음을 나타냅니다. 이 연관관계를 통해 해당 모델을 가져올 수 있습니다.

예를 들어, 각 공급업체에 계정이 하나씩만 있다고 가정하면, 공급업체 모델을 다음과 같이 선언할 수 있습니다:

class Supplier < ApplicationRecord
  has_one :account
end

belongs_to와의 주된 차이점은 링크 열 supplier_id가 다른 테이블에 있다는 것입니다:

has_one 연관관계 다이어그램

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateSuppliers < ActiveRecord::Migration[7.2]
  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

사용 사례에 따라 공급업체 열에 대한 고유 인덱스 및/또는 외래 키 제약 조건을 만들어야 할 수 있습니다. 이 경우 열 정의는 다음과 같이 보일 수 있습니다:

create_table :accounts do |t|
  t.belongs_to :supplier, index: { unique: true }, foreign_key: true
  # ...
end

이 관계는 다른 모델의 belongs_to와 함께 사용하면 양방향이 될 수 있습니다.

has_many 연관관계

has_many 연관관계는 has_one과 유사하지만, 다른 모델과의 일대다 연결을 나타냅니다. 이 연관관계는 종종 belongs_to 연관관계의 “다른 쪽"에서 발견됩니다. 이 연관관계는 모델의 각 인스턴스가 다른 모델의 0개 이상의 인스턴스를 가질 수 있음을 나타냅니다. 예를 들어, 저자와 책이 포함된 애플리케이션에서 저자 모델은 다음과 같이 선언될 수 있습니다:

class Author < ApplicationRecord
  has_many :books
end

참고: 다른 모델의 이름은 has_many 연관관계 선언 시 복수형으로 사용됩니다.

has_many 연관관계 다이어그램

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateAuthors < ActiveRecord::Migration[7.2]
  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

사용 사례에 따라 일반적으로 책 테이블의 author 열에 대한 비고유 인덱스와 선택적으로 외래 키 제약 조건을 만드는 것이 좋습니다:

create_table :books do |t|
  t.belongs_to :author, index: true, foreign_key: true
  # ...
end

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 연관관계 다이어그램

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateAppointments < ActiveRecord::Migration[7.2]
  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

연결 모델의 컬렉션은 has_many 연관관계 메서드를 통해 관리할 수 있습니다. 예를 들어, 다음과 같이 할당하면:

physician.patients = patients

그러면 새로 연결된 객체에 대해 자동으로 새 연결 행이 생성됩니다. 이전에 존재했던 것 중 일부가 이제 없다면 해당 연결 행이 자동으로 삭제됩니다.

경고: 연결 모델의 자동 삭제는 직접적이며 소멸 콜백이 트리거되지 않습니다.

has_many :through 연관관계는 중첩된 has_many 연관관계를 통해 "단축키"를 설정하는 데에도 유용합니다. 예를 들어, 문서에 여러 섹션이 있고 섹션에 여러 단락이 있는 경우, 문서의 모든 단락 컬렉션을 가져오고 싶을 수 있습니다. 다음과 같이 설정할 수 있습니다:

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

through: :sections를 지정하면 Rails는 이제 다음과 같이 이해할 수 있습니다:

@document.paragraphs

has_one :through 연관관계

has_one :through 연관관계는 다른 모델과의 일대일 연결을 설정합니다. 이 연관관계는 선언 모델이 통해 세 번째 모델을 거쳐 다른 모델의 한 인스턴스와 연결될 수 있음을 나타냅니다. 예를 들어, 각 공급업체에 계정이 하나씩 있고, 각 계정이 계정 내역 하나와 연결되어 있다고 가정하면, 공급업체 모델은 다음과 같이 보일 수 있습니다:

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

has_one :through 연관관계 다이어그램

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateAccountHistories < ActiveRecord::Migration[7.2]
  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

    create_table :account_histories do |t|
      t.belongs_to :account
      t.integer :credit_rating
      t.timestamps
    end
  end
end

has_and_belongs_to_many 연관관계

has_and_belongs_to_many 연관관계는 다른 모델과의 직접적인 다대다 연결을 생성합니다. 이 연관관계는 선언 모델의 각 인스턴스가 0개 이상의 다른 모델 인스턴스를 참조할 수 있음을 나타냅니다. 예를 들어, 애플리케이션에 조립품과 부품이 있고, 각 조립품이 여러 부품을 가지며 각 부품이 여러 조립품에 나타난다고 가정하면, 모델을 다음과 같이 선언할 수 있습니다:

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 연관관계 다이어그램

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateAssembliesAndParts < ActiveRecord::Migration[7.2]
  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

    create_table :assemblies_parts, id: false do |t|
      t.belongs_to :assembly
      t.belongs_to :part
    end
  end
end

belongs_tohas_one 중 선택하기

두 모델 간에 일대일 관계를 설정하려면 하나에 belongs_to를, 다른 하나에 has_one을 추가해야 합니다. 어떤 것을 어디에 사용해야 할까요?

차이점은 외래 키가 어디에 있는지(belongs_to 연관관계를 선언한 클래스의 테이블에 있음)에 있지만, 데이터의 실제 의미도 고려해야 합니다. has_one 관계는 무언가가 귀하의 것이라는 것을 말합니다 - 즉, 무언가가 귀하를 가리킵니다. 예를 들어, 공급업체가 계정을 소유한다는 것이 계정이 공급업체를 소유한다는 것보다 더 의미가 있습니다. 이는 다음과 같은 올바른 관계를 제안합니다:

class Supplier < ApplicationRecord
  has_one :account
end

class Account < ApplicationRecord
  belongs_to :supplier
end

해당 마이그레이션은 다음과 같을 수 있습니다:

class CreateSuppliers < ActiveRecord::Migration[7.2]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end

    create_table :accounts do |t|
      t.bigint  :supplier_id
      t.string  :account_number
      t.timestamps
    end

    add_index :accounts, :supplier_id
  end
end

참고: t.bigint :supplier_id를 사용하면 외래 키 이름이 명확하고 명시적입니다. 현재 Rails 버전에서는 t.references :supplier를 사용하여 이 구현 세부 사항을 추상화할 수 있습니다.

has_many :throughhas_and_belongs_to_many 중 선택하기

Rails는 모델 간 다대다 관계를 선언하는 두 가지 다른 방법을 제공합니다. 첫 번째 방법은 has_and_belongs_to_many를 사용하는 것으로, 직접 연관관계를 만들 수 있습니다:

class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end

class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end

두 번째 방법은 has_many :through를 사용하는 것으로, 연결 모델을 통해 간접적으로 연관관계를 만듭니다:

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 관계를 설정해야 한다는 것입니다. 연결 모델에 대해 아무것도 할 필요가 없다면 has_and_belongs_to_many 관계를 설정하는 것이 더 간단할 수 있습니다(단, 데이터베이스에 조인 테이블을 만들어야 한다는 것을 기억해야 합니다).

유효성 검사, 콜백 또는 조인 모델에 대한 추가 속성이 필요한 경우 has_many :through를 사용해야 합니다.

has_and_belongs_to_manyid: false를 통해 기본 키가 없는 조인 테이블을 만들 것을 제안하지만, has_many :through 관계에서는 조인 테이블에 복합 기본 키를 사용하는 것이 좋습니다. 예를 들어, 위의 예제에서 create_table :manifests, primary_key: [:assembly_id, :part_id]를 사용하는 것이 좋습니다.

다형성 연관관계

연관관계에 대한 약간 더 고급 트위스트는 다형성 연관관계입니다. 다형성 연관관계를 사용하면 모델이 단일 연관관계에서 두 개 이상의 다른 모델에 속할 수 있습니다. 예를 들어, 직원 모델 또는 제품 모델에 속할 수 있는 사진 모델이 있을 수 있습니다. 다음과 같이 선언할 수 있습니다:

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

다형성 belongs_to 선언을 인터페이스를 설정하는 것으로 생각할 수 있습니다. 어떤 다른 모델이든 이 인터페이스를 사용할 수 있습니다. @employee.pictures를 통해 직원 인스턴스에서 사진 컬렉션을 가져올 수 있습니다.

마찬가지로 @product.pictures를 통해 제품에서 사진을 가져올 수 있습니다.

Picture 모델의 인스턴스가 있는 경우 @picture.imageable를 통해 해당 부모에 액세스할 수 있습니다. 이를 작동시키려면 다형성 인터페이스를 선언하는 모델에 외래 키 열과 유형 열을 선언해야 합니다:

class CreatePictures < ActiveRecord::Migration[7.2]
  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

이 마이그레이션은 t.references 형식을 사용하여 간소화할 수 있습니다:

class CreatePictures < ActiveRecord::Migration[7.2]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, polymorphic: true
      t.timestamps
    end
  end
end

참고: 다형성 연관관계는 데이터베이스에 클래스 이름을 저장하므로, 해당 데이터는 Ruby 코드에서 사용되는 클래스 이름과 동기화되어야 합니다. 클래스 이름을 변경할 때는 다형성 유형 열의 데이터도 업데이트해야 합니다.

다형성 연관관계 다이어그램

복합 기본 키가 있는 모델 간 연관관계

Rails는 종종 추가 정보 없이도 복합 기본 키가 있는 연관된 모델 간의 기본 키-외래 키 정보를 추론할 수 있습니다. 다음 예를 살펴보겠습니다:

class Order < ApplicationRecord
  self.primary_key = [:shop_id, :id]
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :order
end

여기서 Rails는 has_many / belongs_to 연관관계와 마찬가지로 :id 열을 연관관계의 기본 키로 사용할 것이라고 가정합니다. 외래 키 열이 :order_id라고 추론할 것입니다. 책의 주문 액세스:

order = Order.create!(id: [1, 2], status: "pending")
book = order.books.create!(title: "A Cool Book")

book.reload.order

는 다음과 같은 SQL을 생성하여 주문에 액세스합니다:

SELECT * FROM orders WHERE id = 2

이는 모델의 복합 기본 키에 :id 열이 포함되어 있고, 해당 열이 모든 레코드에 대해 고유한 경우에만 작동합니다. 전체 복합 기본 키를 연관관계에 사용하려면 query_constraints 옵션을 연관관계에 설정하세요. 이 옵션은 연관관계의 복합 외래 키네, 계속해서 번역하겠습니다:

를 지정합니다. 외래 키의 모든 열이 관련 레코드(들)을 쿼리할 때 사용됩니다. 예를 들면 다음과 같습니다:

class Author < ApplicationRecord
  self.primary_key = [:first_name, :last_name]
  has_many :books, query_constraints: [:first_name, :last_name]
end

class Book < ApplicationRecord
  belongs_to :author, query_constraints: [:author_first_name, :author_last_name]
end

책의 저자 액세스:

author = Author.create!(first_name: "Jane", last_name: "Doe")
book = author.books.create!(title: "A Cool Book")

book.reload.author

:first_name :last_name을 사용하여 SQL 쿼리를 실행합니다:

SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'

자기 조인

데이터 모델을 설계할 때 모델이 자신과 관계를 가져야 하는 경우가 있습니다. 예를 들어, 단일 데이터베이스 모델에 모든 직원을 저장하고 싶지만 관리자와 부하 직원 간의 관계를 추적할 수 있어야 합니다. 이 상황은 자기 조인 연관관계로 모델링할 수 있습니다:

class Employee < ApplicationRecord
  has_many :subordinates, class_name: "Employee",
                          foreign_key: "manager_id"

  belongs_to :manager, class_name: "Employee", optional: true
end

이 설정을 통해 @employee.subordinates@employee.manager를 가져올 수 있습니다.

마이그레이션/스키마에서는 모델 자체에 참조 열을 추가합니다.

class CreateEmployees < ActiveRecord::Migration[7.2]
  def change
    create_table :employees do |t|
      t.references :manager, foreign_key: { to_table: :employees }
      t.timestamps
    end
  end
end

참고: to_table 옵션은 SchemaStatements#add_reference에서 설명됩니다.

팁, 요령 및 경고

Rails 애플리케이션에서 액티브 레코드 연관관계를 효율적으로 사용하기 위해 알아두어야 할 몇 가지 사항은 다음과 같습니다:

  • 캐싱 제어
  • 이름 충돌 방지
  • 스키마 업데이트
  • 연관관계 범위 제어
  • 양방향 연관관계

캐싱 제어

모든 연관관계 메서드는 캐싱을 기반으로 구축되어 있어, 가장 최근 쿼리의 결과를 추가 작업에 사용할 수 있습니다. 캐시는 메서드 간에도 공유됩니다. 예를 들어:

# 데이터베이스에서 책을 가져옵니다
author.books.load

# 책 캐시 사본 사용
author.books.size

# 책 캐시 사본 사용
author.books.empty?

그러나 데이터가 애플리케이션의 다른 부분에 의해 변경되었을 수 있으므로 캐시를 다시 로드하고 싶다면 reload를 호출하면 됩니다:

# 데이터베이스에서 책을 가져옵니다
author.books.load

# 책 캐시 사본 사용
author.books.size

# 책 캐시 사본을 폐기하고 데이터베이스로 돌아갑니다
author.books.reload.empty?

이름 충돌 방지

연관관계에 사용할 이름은 자유롭게 선택할 수 없습니다. 연관관계를 생성하면 해당 이름의 메서드가 모델에 추가되므로, ActiveRecord::Base의 인스턴스 메서드와 이름이 겹치면 안 됩니다. 연관관계 메서드가 기본 메서드를 덮어쓰고 문제를 일으킬 수 있습니다. 예를 들어 attributes 또는 connection은 연관관계 이름으로 좋지 않습니다.

스키마 업데이트

연관관계는 매우 유용하지만 마법은 아닙니다. 연관관계에 맞게 데이터베이스 스키마를 유지 관리할 책임이 있습니다. 실제로 이는 두 가지 의미가 있습니다. belongs_to 연관관계의 경우 외래 키를 만들어야 하고, has_and_belongs_to_many 연관관계의 경우 적절한 조인 테이블을 만들어야 합니다.

belongs_to 연관관계에 대한 외래 키 생성

belongs_to 연관관계를 선언할 때 해당하는 외래 키를 만들어야 합니다. 예를 들어 다음 모델을 고려해 보겠습니다:

class Book < ApplicationRecord
  belongs_to :author
end

이 선언에는 책 테이블에 해당하는 외래 키 열이 필요합니다. 새 테이블의 경우 마이그레이션은 다음과 같을 수 있습니다:

class CreateBooks < ActiveRecord::Migration[7.2]
  def change
    create_table :books do |t|
      t.datetime   :published_at
      t.string     :book_number
      t.references :author
    end
  end
end

기존 테이블의 경우에는 다음과 같을 수 있습니다:

class AddAuthorToBooks < ActiveRecord::Migration[7.2]
  def change
    add_reference :books, :author
  end
end

참고: 데이터베이스 수준의 외래 키 무결성을 적용하려면 위의 ‘reference’ 열 선언에 foreign_key: true 옵션을 추가하세요.

has_and_belongs_to_many 연관관계에 대한 조인 테이블 생성

has_and_belongs_to_many 연관관계를 생성하는 경우 명시적으로 조인 테이블을 만들어야 합니다. 조인 테이블 이름이 :join_table 옵션을 사용하여 명시적으로 지정되지 않는 한, Active Record는 클래스 이름의 어휘 순서를 사용하여 이름을 생성합니다. 따라서 저자와 책 모델 간의 조인은 "authors_books"라는 기본 조인 테이블 이름을 사용할 것입니다. 왜냐하면 "a"가 "b"보다 어휘적으로 앞서기 때문입니다.

경고: 모델 이름 간 우선순위는 <=> 연산자를 사용하여 계산됩니다. 즉, 문자열 길이가 다른 경우 가장 짧은 길이까지 문자열이 같으면 더 긴 문자열이 더 높은 어휘적 우선순위로 간주됩니다. 예를 들어 "paperboxes"와 "papers” 테이블 이름을 예상하면 “paperspaperboxes"라는 조인 테이블 이름이 생성될 것 같지만, 실제로는 "paperboxespapers”(밑줄 ‘_'이 ’s'보다 어휘적으로 _작기 때문)라는 조인 테이블 이름이 생성됩니다.

어떤 이름이든, 적절한 마이그레이션을 통해 조인 테이블을 수동으로 생성해야 합니다. 예를 들어 다음과 같은 연관관계를 고려해 보겠습니다:

class Assembly < ApplicationRecord
  has_and_belongs_to_many :parts
end

class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end

이러한 연관관계에는 assemblies_parts 테이블을 생성하는 마이그레이션이 필요합니다. 이 테이블은 기본 키 없이 생성되어야 합니다:

class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.2]
  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

id: falsecreate_table에 전달하는 이유는 이 테이블이 모델을 나타내지 않기 때문입니다. 이는 연관관계가 제대로 작동하도록 하는 데 필요합니다. 만약 이 부분을 잊으면 모순된 ID로 인해 이상한 동작이나 예외가 발생할 수 있습니다.

간단히 하기 위해 create_join_table 메서드를 사용할 수도 있습니다:

class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.2]
  def change
    create_join_table :assemblies, :parts do |t|
      t.index :assembly_id
      t.index :part_id
    end
  end
end

연관관계 범위 제어

기본적으로 연관관계는 현재 모듈의 범위 내에서 객체를 찾습니다. 이는 모델을 모듈 내에 선언할 때 중요할 수 있습니다. 예를 들어:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end

    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end

이렇게 하면 잘 작동합니다. 왜냐하면 SupplierAccount 클래스가 모두 동일한 범위(MyApplication::Business) 내에 정의되었기 때문입니다. 이러한 구조화를 통해 범위를 명시적으로 추가하지 않고도 폴더 기반으로 모델을 구성할 수 있습니다:

# 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가 다른 범위(MyApplication::BusinessMyApplication::Billing)에 정의되었기 때문입니다:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account
    end
  end

  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier
    end
  end
end

다른 네임스페이스의 모델과 연관관계를 설정하려면 연관관계 선언에 전체 클래스 이름을 지정해야 합니다:

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 Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
end

Active Record는 이 두 모델이 양방향 연관관계를 공유하고 있다는 것을 자동으로 식별하려고 시도합니다. 이 정보를 통해 Active Record는 다음을 수행할 수 있습니다:

  • 이미 로드된 데이터에 대한 불필요한 쿼리 방지:

    irb> author = Author.first
    irb> author.books.all? do |book|
    irb>   book.author.equal?(author) # 여기서는 추가 쿼리가 실행되지 않습니다
    irb> end
    => true
    
  • 일관되지 않은 데이터 방지(로드된 Author 객체가 하나뿐이기 때문네, 계속해서 번역하겠습니다:

):

```irb
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
    

Active Record는 대부분의 표준 이름에 대해 자동 식별을 지원합니다. 그러나 :through 또는 :foreign_key 옵션이 포함된 양방향 연관관계는 자동으로 식별되지 않습니다.

반대 연관관계에 대한 사용자 정의 범위도 자동 식별을 방지하며, config.active_record.automatic_scope_inversing이 true(새 애플리케이션의 기본값)로 설정되어 있지 않는 한 연관관계 자체에 대한 사용자 정의 범위도 방지합니다.

예를 들어 다음과 같은 모델 선언을 고려해 보겠습니다:

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.author.equal?(author) # 이렇게 하면 각 책에 대해 author 쿼리가 실행됩니다
    irb> end
    => false
    
  • 일관되지 않은 데이터가 있는 여러 복사본 참조:

    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
    => 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"]
    

Active Record는 :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

:inverse_of 옵션을 has_many 연관관계 선언에 포함하면 Active Record가 양방향 연관관계를 인식하고 위의 첫 번째 예와 같이 동작합니다.

연관관계 참조 상세

다음 섹션에서는 각 유형의 연관관계에 대한 세부 사항을 설명합니다. 여기에는 연관관계가 추가하는 메서드와 연관관계를 선언할 때 사용할 수 있는 옵션이 포함됩니다.

belongs_to 연관관계 참조

데이터베이스 용어로 belongs_to 연관관계는 이 모델의 테이블에 다른 테이블에 대한 참조를 나타내는 열이 포함되어 있음을 의미합니다. 이를 통해 일대일 또는 일대다 관계를 설정할 수 있습니다. 다른 클래스의 테이블에 참조가 있는 경우 일대일 관계라면 대신 has_one을 사용해야 합니다.

belongs_to가 추가하는 메서드

belongs_to 연관관계를 선언하면 선언 클래스에 연관관계와 관련된 8개의 메서드가 자동으로 추가됩니다:

  • association
  • association=(associate)
  • build_association(attributes = {})
  • create_association(attributes = {})
  • create_association!(attributes = {})
  • reload_association
  • reset_association
  • association_changed?
  • association_previously_changed?

이 모든 메서드에서 associationbelongs_to에 전달된 첫 번째 인수로 바뀝니다. 예를 들어 다음과 같은 선언이 있다면:

class Book < ApplicationRecord
  belongs_to :author
end

Book 모델의 각 인스턴스에는 다음과 같은 메서드가 있습니다:

  • author
  • author=
  • build_author
  • create_author
  • create_author!
  • reload_author
  • reset_author
  • author_changed?
  • author_previously_changed?

참고: has_one 또는 belongs_to 연관관계를 초기화할 때는 build_ 접두사를 사용해야 하며, has_many 또는 has_and_belongs_to_many 연관관계에서 사용되는 association.build 메서드를 사용할 수 없습니다. 생성하려면 create_ 접두사를 사용하세요.

association

association 메서드는 연관된 객체(있는 경우)를 반환합니다. 연관된 객체를 찾을 수 없는 경우 nil을 반환합니다.

@author = @book.author

연관된 객체가 이미 이 객체에 대해 데이터베이스에서 검색된 경우 캐시된 버전이 반환됩니다. 이 동작을 무시하고(데이터베이스 읽기를 강제하려면) 부모 객체에서 #reload_association을 호출하세요.

@author = @book.reload_author

연관된 객체의 캐시된 버전을 제거하여(다음 액세스 시 데이터베이스에서 쿼리하도록 하려면) 부모 객체에서 #reset_association을 호출하세요.

@book.reset_author
association=(associate)

association= 메서드는 연관된 객체를 이 객체에 할당합니다. 내부적으로는 연관된 객체의 기본 키를 추출하고 이 객체의 외래 키를 동일한 값으로 설정합니다.

@book.author = @author
build_association(attributes = {})

build_association 메서드는 연관된 유형의 새 객체를 반환합니다. 이 객체는 전달된 속성에서 인스턴스화되며, 이 객체의 외래 키를 통해 링크가 설정됩니다. 그러나 연관된 객체는 아직 저장되지 않습니다.

@author = @book.build_author(author_number: 123,
                             author_name: "John Doe")
create_association(attributes = {})

create_association 메서드는 연관된 유형의 새 객체를 반환합니다. 이 객체는 전달된 속성에서 인스턴스화되고, 이 객체의 외래 키를 통해 링크가 설정됩니다. 그리고 연관된 모델에 지정된 모든 유효성 검사를 통과하면 연관된 객체 저장됩니다.

@author = @book.create_author(author_number: 123,
                              author_name: "John Doe")
create_association!(attributes = {})

create_association과 동일하지만, 레코드가 유효하지 않은 경우 ActiveRecord::RecordInvalid를 발생시킵니다.

association_changed?

association_changed? 메서드는 새 연관된 객체가 할당되었고 다음 저장 시 외래 키가 업데이트될 경우 true를 반환합니다.

@book.author # => #<Author author_number: 123, author_name: "John Doe">
@book.author_changed? # => false

@book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith">
@book.author_changed? # => true

@book.save!
@book.author_changed? # => false
association_previously_changed?

association_previously_changed? 메서드는 이전 저장 시 연관된 객체 참조가 새 객체로 업데이트된 경우 true를 반환합니다.

@book.author # => #<Author author_number: 123, author_name: "John Doe">
@book.author_previously_changed? # => false

@book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith">
@book.save!
@book.author_previously_changed? # => true

belongs_to에 대한 옵션

Rails는 대부분의 상황에서 잘 작동하는 지능적인 기본값을 사용하지만, belongs_to 연관관계 참조의 동작을 사용자 정의해야 하는 경우도 있습니다. 이러한 사용자 정의는 연관관계를 생성할 때 옵션과 범위 블록을 전달하여 쉽게 수행할 수 있습니다. 예를 들어 이 연관관계는 두 가지 옵션을 사용합니다:

class Book < ApplicationRecord
  belongs_to :author, touch: :books_updated_at,
    counter_cache: true
end

belongs_to 연관관계는 다음과 같은 옵션을 지원합니다:

  • :autosave
  • :class_name
  • :counter_cache
  • :default
  • :dependent
  • :ensuring_owner_was
  • :foreign_key
  • :foreign_type
  • :primary_key
  • :inverse_of
  • :optional
  • :polymorphic
  • :required
  • :strict_loading
  • :touch
  • :validate
:autosave

:autosave 옵션을 true로 설정하면 Rails는 부모 객체를 저장할 때 로드된 연관 멤버를 저장하고 삭제 표시된 멤버를 삭제합니다. :autosave 옵션을 false로 설정하는 것은 :autosave 옵션을 설정하지 않는 것과 같지 않습니다. :autosave 옵션이 없는 경우 새 연관 객체는 저장되지만 업데이트된 연관 객체는 저장되지 않습니다.

:class_name

다른 모델의 이름을 연관관계 이름에서 유추할 수 없는 경우 :class_name 옵션을 사용하여 모델 이름을 제공할 수 있습니다. 예를 들어 책이 저자에게 속하지만 실제 저자 모델의 이름이 Patron인 경우 다음과 같이 설정할 수 있습니다:

class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron"
end
:counter_cache

:counter_cache 옵션을 사용하면 속하는 객체 수를 더 효율적으로 찾을 수 있습니다. 다음과 같은 모델을 고려해 보겠습니다:

class Book < ApplicationRecord
  belongs_to :author
end

class Author < ApplicationRecord
  has_many :books
end

이러한 선언에서 @author.books.size의 값을 요청하면 COUNT(*) 쿼리를네, 계속해서 번역하겠습니다:

실행해야 합니다. 이 호출을 피하려면 속하는 모델에 카운터 캐시를 추가할 수 있습니다:

class Book < ApplicationRecord
  belongs_to :author, counter_cache: true
end

class Author < ApplicationRecord
  has_many :books
end

이 선언에서 Rails는 캐시 값을 최신 상태로 유지하고 size 메서드에 해당 값을 반환합니다.

:counter_cache 옵션은 belongs_to 선언에 지정되지만, 실제 열은 연관된(has_many) 모델에 추가해야 합니다. 위의 경우 Author 모델에 books_count 열을 추가해야 합니다.

기본 열 이름 대신 사용자 정의 열 이름을 지정할 수 있습니다. 예를 들어 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 옵션은 belongs_to 쪽 연관관계에만 지정하면 됩니다.

기존 대규모 테이블에서 카운터 캐시 사용을 시작하는 것은 문제가 될 수 있습니다. 열 값을 백필하는 것(테이블을 너무 오래 잠그지 않도록) 및 :counter_cache 사용 전에 수행해야 하며, 그렇지 않으면 size/any?/등과 같은 메서드가 잘못된 카운터 캐시 열 값을 사용하여 잘못된 결과를 생성할 수 있습니다. 자식 레코드 생성/제거와 함께 카운터 캐시 열을 최신 상태로 유지하고, 데이터베이스의 결과만 항상 가져오려면 counter_cache: { active: false }를 사용하세요. 사용자 정의 열 이름도 필요한 경우 counter_cache: { active: false, column: :my_custom_counter }를 사용하세요.

어떤 이유로든 소유자 모델의 기본 키 값을 변경하고 계산된 모델의 외래 키를 업데이트하지 않으면 카운터 캐시가 오래된 데이터를 가질 수 있습니다. 다시 말해, 고아가 된 모델은 여전히 카운터에 포함됩니다. 오래된 카운터 캐시를 수정하려면 reset_counters를 사용하세요.

:default

true로 설정하면 연관관계의 존재가 유효성 검사되지 않습니다.

:dependent

:dependent 옵션을 다음과 같이 설정하면:

  • :destroy는 객체가 삭제될 때 연관된 객체도 destroy됩니다.
  • :delete는 객체가 삭제될 때 연관된 객체가 데이터베이스에서 직접 삭제됩니다(콜백이 실행되지 않음).
  • :destroy_async: 객체가 삭제될 때 ActiveRecord::DestroyAssociationAsyncJob 작업이 대기열에 추가되어 연관된 객체에 대해 destroy가 호출됩니다. 이 기능을 사용하려면 Active Job이 설정되어 있어야 합니다. 데이터베이스의 외래 키 제약 조건이 연관관계에 의해 백업되는 경우에는 이 옵션을 사용하지 마세요. 외래 키 제약 조건 작업은 소유자를 삭제하는 것과 동일한 트랜잭션 내에서 발생합니다.

경고: 다른 클래스의 has_many 연관관계와 연결된 belongs_to 연관관계에 이 옵션을 지정하지 마세요. 그렇지 않으면 데이터베이스에 고아 레코드가 발생할 수 있습니다.

:ensuring_owner_was

소유자에게 호출되는 인스턴스 메서드를 지정합니다. 메서드는 연관된 레코드가 백그라운드 작업에서 삭제되도록 true를 반환해야 합니다.

:foreign_key

관례상 Rails는 이 모델에 대한 외래 키를 보유하는 열의 이름이 연관관계 이름에 _id 접미사가 추가된 것이라고 가정합니다. :foreign_key 옵션을 사용하면 외래 키의 이름을 직접 설정할 수 있습니다:

class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron",
                      foreign_key: "patron_id"
end

팁: 어떤 경우든 Rails는 외래 키 열을 자동으로 생성하지 않습니다. 마이그레이션의 일부로 명시적으로 정의해야 합니다.

:foreign_type

이 연관관계가 다형성 연관관계인 경우 연관된 객체의 유형을 저장하는 열을 지정합니다. 기본적으로 연관관계 이름에 “_type” 접미사가 추가된 이름으로 추정됩니다. 따라서 belongs_to :taggable, polymorphic: true 연관관계를 정의하는 클래스는 기본적으로 “taggable_type"을 :foreign_type로 사용합니다.

:primary_key

관례상 Rails는 id 열을 테이블의 기본 키로 사용한다고 가정합니다. :primary_key 옵션을 사용하면 다른 열을 지정할 수 있습니다.

예를 들어 users 테이블의 기본 키가 guid라고 가정해 보겠습니다. 별도의 todos 테이블에 외래 키 user_idguid 열에 보관하려면 primary_key를 사용하면 됩니다:

class User < ApplicationRecord
  self.primary_key = 'guid' # 기본 키는 id가 아닌 guid
end

class Todo < ApplicationRecord
  belongs_to :user, primary_key: 'guid'
end

@user.todos.create를 실행하면 @todo 레코드의 user_id@userguid 값으로 설정됩니다.

:inverse_of

:inverse_of 옵션은 이 연관관계의 역방향 has_many 또는 has_one 연관관계의 이름을 지정합니다. 양방향 연관관계 섹션에서 자세한 내용을 확인하세요.

class Author < ApplicationRecord
  has_many :books, inverse_of: :author
end

class Book < ApplicationRecord
  belongs_to :author, inverse_of: :books
end
:optional

:optional 옵션을 true로 설정하면 연관된 객체의 존재가 유효성 검사되지 않습니다. 기본적으로 이 옵션은 false로 설정됩니다.

:polymorphic

:polymorphic 옵션을 true로 전달하면 이것이 다형성 연관관계임을 나타냅니다. 다형성 연관관계는 이 가이드의 앞부분에서 자세히 다루었습니다.

:required

true로 설정하면 연관관계 자체의 존재도 유효성 검사됩니다. 이는 ID가 아닌 연관관계 자체를 검증합니다. :inverse_of를 사용하면 추가 쿼리 없이 유효성 검사를 수행할 수 있습니다.

참고: required는 기본적으로 true로 설정되어 있으며 사용되지 않습니다. 연관관계 존재 유효성 검사를 원하지 않는 경우 optional: true를 사용하세요.

:strict_loading

이 연관관계를 통해 연관된 레코드가 로드될 때마다 strict loading을 적용합니다.

:touch

:touch 옵션을 true로 설정하면 이 객체가 저장되거나 삭제될 때 연관된 객체의 updated_at 또는 updated_on 타임스탬프가 현재 시간으로 설정됩니다:

class Book < ApplicationRecord
  belongs_to :author, touch: true
end

class Author < ApplicationRecord
  has_many :books
end

이 경우 책을 저장하거나 삭제하면 연관된 저자의 타임스탬프가 업데이트됩니다. 특정 타임스탬프 속성을 업데이트하도록 지정할 수도 있습니다:

class Book < ApplicationRecord
  belongs_to :author, touch: :books_updated_at
end
:validate

:validate 옵션을 true로 설정하면 이 객체를 저장할 때 새 연관 객체가 유효성 검사됩니다. 기본적으로 false입니다: 이 객체를 저장할 때 새 연관 객체가 유효성 검사되지 않습니다.

belongs_to에 대한 범위

때때로 belongs_to에서 사용되는 쿼리를 사용자 정의하고 싶을 수 있습니다. 이러한 사용자 정의는 범위 블록을 통해 수행할 수 있습니다. 예를 들어:

class Book < ApplicationRecord
  belongs_to :author, -> { where active: true }
end

표준 쿼리 메서드를 범위 블록 내에서 사용할 수 있습니다. 아래에서 설명하는 메서드는 다음과 같습니다:

  • where
  • includes
  • readonly
  • select
where

where 메서드를 사용하면 연관된 객체가 충족해야 하는 조건을 지정할 수 있습니다.

class Book < ApplicationRecord
  belongs_to :author, -> { where active: true }
end
includes

includes 메서드를 사용하면 이 연관관계가 사용될 때 미리 로드해야 하는 두 번째 수준의 연관관계를 지정할 수 있습니다. 예를 들어 다음과 같은 모델을 고려해 보겠습니다:

class Chapter < ApplicationRecord
  belongs_to :book
end

class Book < ApplicationRecord
  belongs_to :author
  has_many :chapters
end

class Author < ApplicationRecord
  has_many :books
end

종종 챕터에서 직접 저자를 가져오는 경우(@chapter.book.author)가 있다면 책에서 저자를 미리 로드하는 것이 코드를 약간 더 효율적으로 만들 수 있습니다:

class Chapter < ApplicationRecord
  belongs_to :book, -> { includes :author }
end

class Book < ApplicationRecord
  belongs_to :author
  has_many :chapters
end

class Author < ApplicationRecord
  has_many :books
end

참고: 즉시 연관관계에는 includes를 사용할 필요가 없습니다. 즉, Book belongs_to :author와 같은 경우 저자는 필요할 때 자동으로 미리 로드됩니다.

readonly

readonly를 사용하면 연관관계를 통해 검색된 연관된 객체는 읽기 전용이 됩니다.

select

select 메서드를 사용하면 연관된 객체에 대한 데이터를 검색하는 SQL SELECT 절을 재정의할 수 있습니다. 기본적으로 Rails는 모든 열을 검색합니다.

팁: belongs_to 연관관계에서 select를 사용하는 경우 올바른 결과를 보장하려면 :foreign_key 옵션도 설정해야 합니다.

연관된 객체가 존재하는지 확인하기

association.nil? 메서드를 사용하여 연관된 객체가 존재하는지 확인할 수 있습니다:

if @book.author.nil?
  @msg = "No author found for this book"
end

객체가 언제 저장되는가?

belongs_to 연관관계에 객체를 할당하면 해당 객체가 자동으로 저장되지 네, 계속해서 번역하겠습니다:

않습니다. 연관된 객체도 자동으로 저장되지 않습니다.

has_one 연관관계 참조

has_one 연관관계는 다른 모델과의 일대일 관계를 생성합니다. 데이터베이스 용어로 이 연관관계는 다른 클래스에 외래 키가 포함되어 있음을 의미합니다. 이 클래스에 외래 키가 포함되어 있다면 대신 belongs_to를 사용해야 합니다.

has_one이 추가하는 메서드

has_one 연관관계를 선언하면 선언 클래스에 연관관계와 관련된 6개의 메서드가 자동으로 추가됩니다:

  • association
  • association=(associate)
  • build_association(attributes = {})
  • create_association(attributes = {})
  • create_association!(attributes = {})
  • reload_association
  • reset_association

이 모든 메서드에서 associationhas_one에 전달된 첫 번째 인수로 바뀝니다. 예를 들어 다음과 같은 선언이 있다면:

class Supplier < ApplicationRecord
  has_one :account
end

Supplier 모델의 각 인스턴스에는 다음과 같은 메서드가 있습니다:

  • account
  • account=
  • build_account
  • create_account
  • create_account!
  • reload_account
  • reset_account

참고: has_one 또는 belongs_to 연관관계를 초기화할 때는 build_ 접두사를 사용해야 하며, has_many 또는 has_and_belongs_to_many 연관관계에서 사용되는 association.build 메서드를 사용할 수 없습니다. 생성하려면 create_ 접두사를 사용하세요.

association

association 메서드는 연관된 객체(있는 경우)를 반환합니다. 연관된 객체를 찾을 수 없는 경우 nil을 반환합니다.

@account = @supplier.account

연관된 객체가 이미 이 객체에 대해 데이터베이스에서 검색된 경우 캐시된 버전이 반환됩니다. 이 동작을 무시하고(데이터베이스 읽기를 강제하려면) 부모 객체에서 #reload_association을 호출하세요.

@account = @supplier.reload_account

연관된 객체의 캐시된 버전을 제거하여(다음 액세스 시 데이터베이스에서 쿼리하도록 하려면) 부모 객체에서 #reset_association을 호출하세요.

@supplier.reset_account
association=(associate)

association= 메서드는 연관된 객체를 이 객체에 할당합니다. 내부적으로는 이 객체의 기본 키를 추출하고 연관된 객체의 외래 키를 동일한 값으로 설정합니다.

@supplier.account = @account
build_association(attributes = {})

build_association 메서드는 연관된 유형의 새 객체를 반환합니다. 이 객체는 전달된 속성에서 인스턴스화되고, 외래 키를 통해 링크가 설정됩니다. 그러나 연관된 객체는 아직 저장되지 않습니다.

@account = @supplier.build_account(terms: "Net 30")
create_association(attributes = {})

create_association 메서드는 연관된 유형의 새 객체를 반환합니다. 이 객체는 전달된 속성에서 인스턴스화되고, 외래 키를 통해 링크가 설정됩니다. 그리고 연관된 모델에 지정된 모든 유효성 검사를 통과하면 연관된 객체 저장됩니다.

@account = @supplier.create_account(terms: "Net 30")
create_association!(attributes = {})

create_association과 동일하지만, 레코드가 유효하지 않은 경우 ActiveRecord::RecordInvalid를 발생시킵니다.

has_one에 대한 옵션

Rails는 대부분의 상황에서 잘 작동하는 지능적인 기본값을 사용하지만, has_one 연관관계 참조의 동작을 사용자 정의해야 하는 경우도 있습니다. 이러한 사용자 정의는 연관관계를 생성할 때 옵션을 전달하여 쉽게 수행할 수 있습니다. 예를 들어 이 연관관계는 두 가지 옵션을 사용합니다:

class Supplier < ApplicationRecord
  has_one :account, class_name: "Billing", dependent: :nullify
end

has_one 연관관계는 다음과 같은 옵션을 지원합니다:

  • :as
  • :autosave
  • :class_name
  • :dependent
  • :disable_joins
  • :ensuring_owner_was
  • :foreign_key
  • :inverse_of
  • :primary_key
  • :query_constraints
  • :required
  • :source
  • :source_type
  • :strict_loading
  • :through
  • :touch
  • :validate
:as

:as 옵션을 설정하면 이것이 다형성 연관관계임을 나타냅니다. 다형성 연관관계는 이 가이드의 앞부분에서 자세히 다루었습니다.

:autosave

:autosave 옵션을 true로 설정하면 Rails는 로드된 연관 멤버를 저장하고 삭제 표시된 멤버를 삭제할 것입니다. :autosave 옵션을 false로 설정하는 것은 :autosave 옵션을 설정하지 않는 것과 같지 않습니다. :autosave 옵션이 없는 경우 새 연관 객체는 저장되지만 업데이트된 연관 객체는 저장되지 않습니다.

:class_name

다른 모델의 이름을 연관관계 이름에서 유추할 수 없는 경우 :class_name 옵션을 사용하여 모델 이름을 제공할 수 있습니다. 예를 들어 공급업체에 계정이 있지만 실제 계정 모델의 이름이 Billing인 경우 다음과 같이 설정할 수 있습니다:

class Supplier < ApplicationRecord
  has_one :account, class_name: "Billing"
end
:dependent

연관된 객체가 소유자와 함께 삭제될 때의 동작을 제어합니다:

  • :destroy는 연관된 객체도 삭제됩니다
  • :delete는 연관된 객체가 데이터베이스에서 직접 삭제됩니다(콜백이 실행되지 않음)
  • :destroy_async: 객체가 삭제될 때 ActiveRecord::DestroyAssociationAsyncJob 작업이 대기열에 추가되어 연관된 객체에 대해 destroy가 호출됩니다. 이 기능을 사용하려면 Active Job이 설정되어 있어야 합니다. 데이터베이스의 외래 키 제약 조건이 연관관계에 의해 백업되는 경우에는 이 옵션을 사용하지 마세요. 외래 키 제약 조건 작업은 소유자를 삭제하는 것과 동일한 트랜잭션 내에서 발생합니다.
  • :nullify는 외래 키가 NULL로 설정됩니다. 다형성 연관관계의 경우 유형 열도 null로 설정됩니다. 콜백이 실행되지 않습니다.
  • :restrict_with_exception은 연관된 레코드가 있는 경우 ActiveRecord::DeleteRestrictionError 예외가 발생합니다
  • :restrict_with_error은 연관된 객체가 있는 경우 소유자에게 오류가 추가됩니다

NOT NULL 데이터베이스 제약 조건이 있는 연관관계에 대해서는 :nullify 옵션을 설정하거나 남겨두지 마세요. 연관된 객체를 삭제하지 않으려면 dependent를 destroy로 설정해야 합니다. 그렇지 않으면 초기 연관된 객체의 외래 키가 허용되지 않는 NULL 값으로 설정되어 연관된 객체를 변경할 수 없습니다.

:disable_joins

연관관계에 대한 조인을 건너뛸지 여부를 지정합니다. true로 설정하면 두 개 이상의 쿼리가 생성됩니다. 일부 경우에는 정렬 또는 제한이 적용되면 데이터베이스 제한으로 인해 메모리 내에서 수행됩니다. 이 옵션은 has_one :through 연관관계에만 적용되며 has_one 단독으로는 조인을 수행하지 않습니다.

:ensuring_owner_was

소유자에게 호출되는 인스턴스 메서드를 지정합니다. 메서드는 연관된 레코드가 백그라운드 작업에서 삭제되도록 true를 반환해야 합니다.

:foreign_key

관례상 Rails는 다른 모델의 외래 키 열 이름이 이 모델 이름에 _id 접미사가 추가된 것이라고 가정합니다. :foreign_key 옵션을 사용하면 외래 키의 이름을 직접 설정할 수 있습니다:

class Supplier < ApplicationRecord
  has_one :account, foreign_key: "supp_id"
end

팁: 어떤 경우든 Rails는 외래 키 열을 자동으로 생성하지 않습니다. 마이그레이션의 일부로 명시적으로 정의해야 합니다.

:inverse_of

:inverse_of 옵션은 이 연관관계의 역방향 belongs_to 연관관계의 이름을 지정합니다. 양방향 연관관계 섹션에서 자세한 내용을 확인하세요.

class Supplier < ApplicationRecord
  has_one :account, inverse_of: :supplier
end

class Account < ApplicationRecord
  belongs_to :supplier, inverse_of: :account
end
:primary_key

관례상 Rails는 이 모델의 기본 키 열이 id라고 가정합니다. :primary_key 옵션을 사용하면 다른 열을 지정할 수 있습니다.

:query_constraints

복합 외래 키 역할을 합니다. 연관된 객체를 쿼리하는 데 사용할 열 목록을 정의합니다. 이 옵션은 선택 사항입니다. 기본적으로 Rails는 값을 자동으로 유추하려고 합니다. 값을 설정하는 경우 배열 크기는 연관 모델의 기본 키 또는 query_constraints 크기와 일치해야 합니다.

:required

true로 설정하면 연관관계 자체의 존재도 유효성 검사됩니다. 이는 ID가 아닌 연관관계 자체를 검증합니다. :inverse_of를 사용하면 추가 쿼리 없이 유효성 검사를 수행할 수 있습니다.

:source

:source 옵션은 has_one :through 연관관계의 소스 연관관계 이름을 지정합니다.

:source_type

:source_type 옵션은 다형성 연관관계를 통해 진행되는 has_one :through 연관관계의 소스 연관관계 유형을 지정합니다.

class Author < ApplicationRecord
  has_one :book
  has_one :hardback, through: :book, source: :format, source_type: "Hardback"
  has_one :dust_jacket, through: :hardback
end

class Book < ApplicationRecord
  belongs_to :format, polymorphic: true
end

class Paperback < ApplicationRecord; end

class Hardback < ApplicationRecord
  has_one :dust_jacket
end

class DustJacket < ApplicationRecord; end
:strict_loading

이 연관관계를 통해 연관된 레코드가 로드될 때마다 strict loading을 적용합니다.

:through

:through 옵션은네, 계속해서 번역하겠습니다:

쿼리를 수행할 조인 모델을 지정합니다. has_one :through 연관관계는 이 가이드의 앞부분에서 자세히 다루었습니다.

:touch

:touch 옵션을 true로 설정하면 이 객체가 저장되거나 삭제될 때 연관된 객체의 updated_at 또는 updated_on 타임스탬프가 현재 시간으로 설정됩니다:

class Supplier < ApplicationRecord
  has_one :account, touch: true
end

class Account < ApplicationRecord
  belongs_to :supplier
end

이 경우 공급업체를 저장하거나 삭제하면 연관된 계정의 타임스탬프가 업데이트됩니다. 특정 타임스탬프 속성을 업데이트하도록 지정할 수도 있습니다:

class Supplier < ApplicationRecord
  has_one :account, touch: :suppliers_updated_at
end
:validate

:validate 옵션을 true로 설정하면 이 객체를 저장할 때 새 연관 객체가 유효성 검사됩니다. 기본적으로 false입니다: 이 객체를 저장할 때 새 연관 객체가 유효성 검사되지 않습니다.

has_one에 대한 범위

때때로 has_one에서 사용되는 쿼리를 사용자 정의하고 싶을 수 있습니다. 이러한 사용자 정의는 범위 블록을 통해 수행할 수 있습니다. 예를 들어:

class Supplier < ApplicationRecord
  has_one :account, -> { where active: true }
end

표준 쿼리 메서드를 범위 블록 내에서 사용할 수 있습니다. 아래에서 설명하는 메서드는 다음과 같습니다:

  • where
  • includes
  • readonly
  • select
where

where 메서드를 사용하면 연관된 객체가 충족해야 하는 조건을 지정할 수 있습니다.

class Supplier < ApplicationRecord
  has_one :account, -> { where "confirmed = 1" }
end
includes

includes 메서드를 사용하면 이 연관관계가 사용될 때 미리 로드해야 하는 두 번째 수준의 연관관계를 지정할 수 있습니다. 예를 들어 다음과 같은 모델을 고려해 보겠습니다:

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.account.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
readonly

readonly를 사용하면 연관관계를 통해 검색된 연관된 객체는 읽기 전용이 됩니다.

select

select 메서드를 사용하면 연관된 객체에 대한 데이터를 검색하는 SQL SELECT 절을 재정의할 수 있습니다. 기본적으로 Rails는 모든 열을 검색합니다.

연관된 객체가 존재하는지 확인하기

association.nil? 메서드를 사용하여 연관된 객체가 존재하는지 확인할 수 있습니다:

if @supplier.account.nil?
  @msg = "No account found for this supplier"
end

객체가 언제 저장되는가?

has_one 연관관계에 객체를 할당하면 해당 객체가 자동으로 저장됩니다(외래 키를 업데이트하기 위해). 여러 객체를 한 문장에 할당하면 모두 저장됩니다.

이러한 저장 중 하나라도 유효성 검사 오류로 인해 실패하면 할당 문이 false를 반환하고 할당 자체가 취소됩니다.

부모 객체(has_one 연관관계를 선언한 객체)가 저장되지 않은 경우(new_record?true를 반환) 연관 객체는 추가될 때 저장되지 않습니다. 부모가 저장될 때 연관관계의 모든 저장되지 않은 멤버가 자동으로 저장됩니다.

has_one 연관관계에 객체를 할당하되 객체를 저장하지 않으려면 build_association 메서드를 사용하세요.

has_many 연관관계 참조

has_many 연관관계는 다른 모델과의 일대다 관계를 생성합니다. 데이터베이스 용어로 이 연관관계는 다른 클래스에 이 클래스를 참조하는 외래 키가 포함되어 있음을 의미합니다.

has_many가 추가하는 메서드

has_many 연관관계를 선언하면 선언 클래스에 연관관계와 관련된 17개의 메서드가 자동으로 추가됩니다:

이 모든 메서드에서 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
books.empty?
books.size
books.find(...)
books.where(...)
books.exists?(...)
books.build(attributes = {}, ...)
books.create(attributes = {})
books.create!(attributes = {})
books.reload
collection

collection 메서드는 연관된 모든 객체의 Relation을 반환합니다. 연관된 객체가 없는 경우 빈 Relation을 반환합니다.

@books = @author.books
collection<<(object, ...)

collection<< 메서드는 하나 이상의 객체를 컬렉션에 추가합니다. 이 메서드는 객체의 외래 키를 호출 모델의 기본 키로 설정합니다.

@author.books << @book1
collection.delete(object, ...)

collection.delete 메서드는 하나 이상의 객체를 컬렉션에서 제거합니다. 이 메서드는 객체의 외래 키를 NULL로 설정합니다.

@author.books.delete(@book1)

경고: 또한 dependent: :destroy와 연결된 객체는 삭제되고, dependent: :delete_all과 연결된 객체는 삭제됩니다.

collection.destroy(object, ...)

collection.destroy 메서드는 하나 이상의 객체를 컬렉션에서 제거합니다. 이 메서드는 각 객체에 대해 destroy를 실행합니다.

@author.books.destroy(@book1)

경고: 객체는 항상 데이터베이스에서 제거되며, :dependent 옵션은 무시됩니다.

collection=(objects)

collection= 메서드는 추가 및 삭제를 통해 컬렉션이 제공된 객체만 포함하도록 만듭니다. 변경 사항은 데이터베이스에 영구적으로 저장됩니다.

collection_singular_ids

collection_singular_ids 메서드는 컬렉션의 객체 ID 배열을 반환합니다.

@book_ids = @author.book_ids
collection_singular_ids=(ids)

collection_singular_ids= 메서드는 추가 및 삭제를 통해 제공된 기본 키 값으로 식별된 객체만 포함하도록 컬렉션을 만듭니다. 변경 사항은 데이터베이스에 영구적으로 저장됩니다.

collection.clear

collection.clear 메서드는 dependent 옵션에 지정된 전략에 따라 컬렉션의 모든 객체를 제거합니다. 옵션이 지정되지 않은 경우 기본 전략을 따릅니다. has_many :through 연관관계의 기본 전략은 delete_all이고, has_many 연관관계의 기본 전략은 외래 키를 NULL로 설정하는 것입니다.

@author.books.clear

경고: dependent: :destroy 또는 dependent: :destroy_async와 연결된 객체는 dependent: :delete_all과 마찬가지로 삭제됩니다.

collection.empty?

collection.empty? 메서드는 컬렉션에 연관된 객체가 없는 경우 true를 반환합니다.

<% if @author.books.empty? %>
  No Books Found
<% end %>
collection.size

collection.size 메서드는 컬렉션의 객체 수를 반환합니다.

@book_count = @author.books.size
collection.find(...)

collection.find 메서드는 컬렉션의 테이블에서 객체를 찾습니다.

@available_book = @author.books.find(1)
collection.where(...)

collection.where 메서드는 제공된 조건에 따라 컬렉션 내의 객체를 찾지만, 객체는 지연 로드되므로 데이터베이스 쿼리는 객체에 액세스할 때만 실행됩니다.

@available_books = @author.books.where(available: true) # 아직 쿼네, 계속해서 번역하겠습니다:

리가 실행되지 않음
@available_book = @available_books.first # 이제 데이터베이스가 쿼리됨
collection.exists?(...)

collection.exists? 메서드는 제공된 조건을 충족하는 객체가 컬렉션의 테이블에 존재하는지 확인합니다.

collection.build(attributes = {})

collection.build 메서드는 연관된 유형의 단일 또는 배열 객체를 반환합니다. 객체는 전달된 속성에서 인스턴스화되고 외래 키를 통해 링크가 생성되지만, 연관된 객체는 아직 저장되지 않습니다.

@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 = {})

collection.create 메서드는 연관된 유형의 단일 또는 배열 객체를 반환합니다. 객체는 전달된 속성에서 인스턴스화되고 외래 키를 통해 링크가 생성됩니다. 그리고 연관된 모델에 지정된 모든 유효성 검사를 통과하면 연관된 객체 저장됩니다.

@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!(attributes = {})

collection.create와 동일하지만, 레코드가 유효하지 않은 경우 ActiveRecord::RecordInvalid를 발생시킵니다.

collection.reload

collection.reload 메서드는 데이터베이스 읽기를 강제하여 연관된 모든 객체의 Relation을 반환합니다. 연관된 객체가 없는 경우 빈 Relation을 반환합니다.

@books = @author.books.reload

has_many에 대한 옵션

Rails는 대부분의 상황에서 잘 작동하는 지능적인 기본값을 사용하지만, has_many 연관관계 참조의 동작을 사용자 정의해야 하는 경우도 있습니다. 이러한 사용자 정의는 연관관계를 생성할 때 옵션을 전달하여 쉽게 수행할 수 있습니다. 예를 들어 이 연관관계는 두 가지 옵션을 사용합니다:

class Author < ApplicationRecord
  has_many :books, dependent: :delete_all, validate: false
end

has_many 연관관계는 다음과 같은 옵션을 지원합니다:

  • :as
  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :disable_joins
  • :ensuring_owner_was
  • :extend
  • :foreign_key
  • :foreign_type
  • :inverse_of
  • :primary_key
  • :query_constraints
  • :source
  • :source_type
  • :strict_loading
  • :through
  • :validate
:as

:as 옵션을 설정하면 이것이 다형성 연관관계임을 나타냅니다. 다형성 연관관계는 이 가이드의 앞부분에서 자세히 다루었습니다.

:autosave

:autosave 옵션을 true로 설정하면 Rails는 로드된 연관 멤버를 저장하고 삭제 표시된 멤버를 삭제할 것입니다. :autosave 옵션을 false로 설정하는 것은 :autosave 옵션을 설정하지 않는 것과 같지 않습니다. :autosave 옵션이 없는 경우 새 연관 객체는 저장되지만 업데이트된 연관 객체는 저장되지 않습니다.

:class_name

다른 모델의 이름을 연관관계 이름에서 유추할 수 없는 경우 :class_name 옵션을 사용하여 모델 이름을 제공할 수 있습니다. 예를 들어 저자에게 많은 책이 있지만 실제 책 모델의 이름이 Transaction인 경우 다음과 같이 설정할 수 있습니다:

class Author < ApplicationRecord
  has_many :books, class_name: "Transaction"
end
:counter_cache

이 옵션을 사용하여 사용자 정의 :counter_cache를 구성할 수 있습니다. belongs_to 연관관계에서 :counter_cache 이름을 사용자 정의한 경우에만 이 옵션이 필요합니다.

:dependent

연관된 객체가 소유자와 함께 삭제될 때의 동작을 제어합니다:

  • :destroy는 연관된 모든 객체도 삭제됩니다
  • :delete_all은 연관된 모든 객체가 데이터베이스에서 직접 삭제됩니다(콜백이 실행되지 않음)
  • :destroy_async: 객체가 삭제될 때 ActiveRecord::DestroyAssociationAsyncJob 작업이 대기열에 추가되어 연관된 객체에 대해 destroy가 호출됩니다. 이 기능을 사용하려면 Active Job이 설정되어 있어야 합니다.
  • :nullify는 외래 키가 NULL로 설정됩니다. 다형성 연관관계의 경우 유형 열도 null로 설정됩니다. 콜백이 실행되지 않습니다.
  • :restrict_with_exception은 연관된 레코드가 있는 경우 ActiveRecord::DeleteRestrictionError 예외가 발생합니다
  • :restrict_with_error은 연관된 객체가 있는 경우 소유자에게 오류가 추가됩니다

:destroy:delete_all 옵션은 collection.deletecollection= 메서드의 의미론에도 영향을 미쳐, 컬렉션에서 제거될 때 연관된 객체가 삭제됩니다.

:disable_joins

연관관계에 대한 조인을 건너뛸지 여부를 지정합니다. true로 설정하면 두 개 이상의 쿼리가 생성됩니다. 일부 경우에는 정렬 또는 제한이 적용되면 데이터베이스 제한으로 인해 메모리 내에서 수행됩니다. 이 옵션은 has_many :through 연관관계에만 적용되며 has_many 단독으로는 조인을 수행하지 않습니다.

:ensuring_owner_was

소유자에게 호출되는 인스턴스 메서드를 지정합니다. 메서드는 연관된 레코드가 백그라운드 작업에서 삭제되도록 true를 반환해야 합니다.

:extend

연관관계 객체에 확장될 모듈 또는 모듈 배열을 지정합니다. 연관관계에 메서드를 정의할 때 특히 유용하며, 여러 연관관계 객체 간에 공유되어야 할 때 유용합니다.

:foreign_key

관례상 Rails는 다른 모델의 외래 키 열 이름이 이 모델 이름에 _id 접미사가 추가된 것이라고 가정합니다. :foreign_key 옵션을 사용하면 외래 키의 이름을 직접 설정할 수 있습니다:

class Author < ApplicationRecord
  has_many :books, foreign_key: "cust_id"
end

팁: 어떤 경우든 Rails는 외래 키 열을 자동으로 생성하지 않습니다. 마이그레이션의 일부로 명시적으로 정의해야 합니다.

:foreign_type

이 연관관계가 다형성 연관관계인 경우 연관된 객체의 유형을 저장하는 열을 지정합니다. 기본적으로 ”as“ 옵션에 지정된 다형성 연관관계의 이름에 ”_type“ 접미사가 추가된 이름으로 추정됩니다. 따라서 has_many :tags, as: :taggable 연관관계를 정의하는 클래스는 기본적으로 ”taggable_type“을 :foreign_type로 사용합니다.

:inverse_of

:inverse_of 옵션은 이 연관관계의 역방향 belongs_to 연관관계의 이름을 지정합니다. 양방향 연관관계 섹션에서 자세한 내용을 확인하세요.

:primary_key

관례상 Rails는 연관 모델의 기본 키 열이 id라고 가정합니다. :primary_key 옵션을 사용하면 다른 열을 명시적으로 지정할 수 있습니다.

예를 들어 users 테이블의 기본 키가 id가 아닌 guid라고 가정해 보겠습니다. todos 테이블에 외래 키 user_idguid 열에 보관하려면 :primary_key를 사용하면 됩니다:

class User < ApplicationRecord
  has_many :todos, primary_key: :guid
end
:query_constraints

복합 외래 키 역할을 합니다. 연관된 객체를 쿼리하는 데 사용할 열 목록을 정의합니다. 이 옵션은 선택 사항입니다. 기본적으로 Rails는 값을 자동으로 유추하려고 합니다. 값을 설정하는 경우 배열 크기는 연관 모델의 기본 키 또는 query_constraints 크기와 일치해야 합니다.

:source

:source 옵션은 has_many :through 연관관계의 소스 연관관계 이름을 지정합니다.

:source_type

:source_type 옵션은 다형성 연관관계를 통해 진행되는 has_many :through 연관관계의 소스 연관관계 유형을 지정합니다.

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
:strict_loading

true로 설정하면 이 연관관계를 통해 연관된 레코드가 로드될 때마다 strict loading을 적용합니다.

:through

:through 옵션은 쿼리를 수행할 조인 모델을 지정합니다. has_many :through 연관관계는 이 가이드의 앞부분에서 다루었던 다대다 관계를 구현하는 방법을 제공합니다.

:validate

:validate 옵션을 false로 설정하면 이 객체를 저장할 때 새 연관 객체가 유효성 검사되지 않습니다. 기본적으로 true입니다: 이 객체를 저장할 때 새 연관 객체가 유효성 검사됩니다.

has_many에 대한 범위

때때로 has_many에서 사용되는 쿼리를 사용자 정의하고 싶을 수 있습니다. 이러한 사용자 정의는 범위 블록을 통해 수행할 수 있습니다. 예를 들어:

class Author < ApplicationRecord
  has_many :books, -> { where processed: true }
end

표준 쿼리 메서드를 범위 블록 내에서 사용할 수 있습니다. 아래에서 설명하는 메서드는 다음과 같습니다:

  • where
  • extending
  • group
  • includes
  • limit
  • offset
  • order
  • readonly
  • select
  • distinct
where

where 메서드를 사용하면 연관된 객체가 충족해야 하는 조건을 지네, 계속해서 번역하겠습니다:

정할 수 있습니다.

class Author < ApplicationRecord
  has_many :confirmed_books, -> { where "confirmed = 1" },
    class_name: "Book"
end

해시 스타일 where 옵션을 사용할 수도 있습니다:

class Author < ApplicationRecord
  has_many :confirmed_books, -> { where confirmed: true },
    class_name: "Book"
end

해시 스타일 where를 사용하면 이 연관관계를 통한 레코드 생성이 자동으로 해시 범위에 의해 제한됩니다. 이 경우 @author.confirmed_books.create 또는 @author.confirmed_books.build를 사용하면 confirmed 열이 true인 책이 생성됩니다.

extending

extending 메서드는 연관관계 프록시를 확장할 명명된 모듈을 지정합니다. 연관관계 확장은 이 가이드의 뒷부분에서 자세히 다룹니다.

group

group 메서드는 결과 집합을 그룹화할 속성 이름을 제공합니다. SQL의 GROUP BY 절을 사용합니다.

class Author < ApplicationRecord
  has_many :chapters, -> { group 'books.id' },
                      through: :books
end
includes

includes 메서드를 사용하면 이 연관관계가 사용될 때 미리 로드해야 하는 두 번째 수준의 연관관계를 지정할 수 있습니다. 예를 들어 다음과 같은 모델을 고려해 보겠습니다:

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :author
  has_many :chapters
end

class Chapter < ApplicationRecord
  belongs_to :book
end

종종 저자에서 직접 챕터를 가져오는 경우(@author.books.chapters)가 있다면 책에서 챕터를 미리 로드하는 것이 코드를 약간 더 효율적으로 만들 수 있습니다:

class Author < ApplicationRecord
  has_many :books, -> { includes :chapters }
end

class Book < ApplicationRecord
  belongs_to :author
  has_many :chapters
end

class Chapter < ApplicationRecord
  belongs_to :book
end
limit

limit 메서드를 사용하면 연관관계를 통해 가져올 객체 수를 제한할 수 있습니다.

class Author < ApplicationRecord
  has_many :recent_books,
    -> { order('published_at desc').limit(100) },
    class_name: "Book"
end
offset

offset 메서드를 사용하면 연관관계를 통해 객체를 가져올 때 시작 오프셋을 지정할 수 있습니다. 예를 들어 -> { offset(11) }은 처음 11개 레코드를 건너뜁니다.

order

order 메서드는 연관된 객체가 수신되는 순서를 결정합니다(SQL ORDER BY 절 구문 사용).

class Author < ApplicationRecord
  has_many :books, -> { order "date_confirmed DESC" }
end
readonly

readonly를 사용하면 연관관계를 통해 검색된 연관된 객체는 읽기 전용이 됩니다.

select

select 메서드를 사용하면 연관된 객체에 대한 데이터를 검색하는 SQL SELECT 절을 재정의할 수 있습니다. 기본적으로 Rails는 모든 열을 검색합니다.

경고: 직접 select를 지정하는 경우 연관 모델의 기본 키와 외래 키 열을 반드시 포함해야 합니다. 그렇지 않으면 Rails에서 오류가 발생합니다.

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>]

위의 경우 두 개의 읽기 기록이 있지만 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>]

위의 경우 여전히 두 개의 읽기 기록이 있습니다. 그러나 person.articles는 고유한 레코드만 표시합니다.

영구 연관관계에 고유성을 보장하려면 테이블 자체에 고유 인덱스를 추가해야 합니다. 예를 들어 readings 테이블이 있고 사람당 기사를 한 번만 추가할 수 있도록 하려면 다음과 같은 마이그레이션을 추가할 수 있습니다:

add_index :readings, [:person_id, :article_id], unique: true

그러면 기사를 사람에게 두 번 추가하려고 하면 ActiveRecord::RecordNotUnique 오류가 발생합니다:

irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
ActiveRecord::RecordNotUnique

include?를 사용하여 고유성을 확인하려고 하면 경쟁 상태가 발생할 수 있습니다. 위의 기사 예에서와 같이 include?를 사용하여 고유성을 적용하려고 하지 마세요. 여러 사용자가 동시에 이 작업을 시도할 수 있기 때문입니다:

person.articles << article unless person.articles.include?(article)

객체가 언제 저장되는가?

has_many 연관관계에 객체를 할당하면 해당 객체가 자동으로 저장됩니다(외래 키를 업데이트하기 위해). 한 문장에서 여러 객체를 할당하면 모두 저장됩니다.

이러한 저장 중 하나라도 유효성 검사 오류로 인해 실패하면 할당 문이 false를 반환하고 할당 자체가 취소됩니다.

부모 객체(has_many 연관관계를 선언한 객체)가 저장되지 않은 경우(new_record?true를 반환) 연관 객체는 추가될 때 저장되지 않습니다. 부모가 저장될 때 연관관계의 모든 저장되지 않은 멤버가 자동으로 저장됩니다.

has_many 연관관계에 객체를 할당하되 객체를 저장하지 않으려면 collection.build 메서드를 사용하세요.

has_and_belongs_to_many 연관관계 참조

has_and_belongs_to_many 연관관계는 다른 모델과의 직접적인 다대다 관계를 생성합니다. 이 연관관계는 선언 모델의 각 인스턴스가 0개 이상의 다른 모델 인스턴스를 참조할 수 있음을 나타냅니다.

has_and_belongs_to_many가 추가하는 메서드

has_and_belongs_to_many 연관관계를 선언하면 선언 클래스에 연관관계와 관련된 여러 메서드가 자동으로 추가됩니다:

이 모든 메서드에서 collectionhas_and_belongs_to_many에 전달된 첫 번째 인수로 바뀌고, collection_singular는 해당 기호의 단수형 버전으로 바뀝니다. 예를 들어 다음과 같은 선언이 있다면:

class Part < ApplicationRecord
  has_and_belongs_to_many :assemblies
end

Part 모델의 각 인스턴스에는 다음과 같은 메서드가 있습니다:

assemblies
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
assemblies=(objects)
assembly_ids
assembly_ids=(ids)
assemblies.clear
assemblies.empty?
assemblies.size
assemblies.find(...)
assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
assemblies.reload
추가 열 메서드

has_and_belongs_to_many 연관관계의 조인 테이블에 두 개의 외래 키 이외에 추가 열이 있는 경우, 해당 열이 해당 연관관계를 통해 검색된 레코드에 속성으로 추가됩니다. 추가 속성이 있는 레코드는 항상 읽기 전용이 됩니다. 왜냐하면 Rails는 이러한 속성의 변경 사항을 저장할 수 없기 때문입니다.

경고: has_and_belongs_to_many 연관관계의 조인 테이블에 추가 속성을 사용하는 것은 권장되지 않습니다. 두 모델 간의 다대다 관계를 연결하는 테이블에 이러한 복잡한 동작이 필요한 경우 has_many :through 연관관계를 사용해야 합니다.

collection

collection 메서드는 연관된 모든 객체의 Relation을 반환합니다. 연관된 객체가 없는 경우 빈 Relation을 반환합니다.

@assemblies = @part.assemblies
collection<<(object, ...)

collection<< 메서드는 하나 이상의 객체를 컬렉션에 추가합니다. 이 메서드는 조인 테이블에 레코드를 생성합니다.

@part.assemblies << @assembly1

참고: 이 메서드는 collection.concatcollection.push의 별칭입니다.

collection.delete(object, ...)

collection.delete 메서드는 하나 이상의 객체를 컬렉션에서 제거합니다. 이 메서드는 객체를 삭제하지 않고 조인 테이블의 레코드를 삭제합니다.

@part.assemblies.delete(@assembly1)
collection.destroy(object, ...)

collection.destroy 메서드는 하나 이상의 객체를 컬렉션에서 제거합니다. 이 메서드는 객체를 삭제하지 않고 조인 테이블의 레코드를 삭제합니다.

@part.assemblies.destroy(@assembly1)
collection=(objects)

collection= 메서드는 추가 및 삭제를 통해 컬렉션이 제공된 객체만 포함하도록 만듭니다. 변경 사항은 데이터베이스에 영구적으로 저장됩네, 계속해서 번역하겠습니다:

니다.

collection_singular_ids

collection_singular_ids 메서드는 컬렉션의 객체 ID 배열을 반환합니다.

@assembly_ids = @part.assembly_ids
collection_singular_ids=(ids)

collection_singular_ids= 메서드는 추가 및 삭제를 통해 제공된 기본 키 값으로 식별된 객체만 포함하도록 컬렉션을 만듭니다. 변경 사항은 데이터베이스에 영구적으로 저장됩니다.

collection.clear

collection.clear 메서드는 조인 테이블의 행을 삭제하여 컬렉션에서 모든 객체를 제거합니다. 이 메서드는 연관된 객체를 삭제하지 않습니다.

collection.empty?

collection.empty? 메서드는 컬렉션에 연관된 객체가 없는 경우 true를 반환합니다.

<% if @part.assemblies.empty? %>
  This part is not used in any assemblies
<% end %>
collection.size

collection.size 메서드는 컬렉션의 객체 수를 반환합니다.

@assembly_count = @part.assemblies.size
collection.find(...)

collection.find 메서드는 컬렉션의 테이블에서 객체를 찾습니다.

@assembly = @part.assemblies.find(1)
collection.where(...)

collection.where 메서드는 제공된 조건에 따라 컬렉션 내의 객체를 찾지만, 객체는 지연 로드되므로 데이터베이스 쿼리는 객체에 액세스할 때만 실행됩니다.

@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
collection.exists?(...)

collection.exists? 메서드는 제공된 조건을 충족하는 객체가 컬렉션의 테이블에 존재하는지 확인합니다.

collection.build(attributes = {})

collection.build 메서드는 연관된 유형의 새 객체를 반환합니다. 이 객체는 전달된 속성에서 인스턴스화되고 조인 테이블을 통해 링크가 생성됩니다. 그러나 연관된 객체는 아직 저장되지 않습니다.

@assembly = @part.assemblies.build({ assembly_name: "Transmission housing" })
collection.create(attributes = {})

collection.create 메서드는 연관된 유형의 새 객체를 반환합니다. 이 객체는 전달된 속성에서 인스턴스화되고 조인 테이블을 통해 링크가 생성됩니다. 그리고 연관된 모델에 지정된 모든 유효성 검사를 통과하면 연관된 객체 저장됩니다.

@assembly = @part.assemblies.create({ assembly_name: "Transmission housing" })
collection.create!(attributes = {})

collection.create와 동일하지만, 레코드가 유효하지 않은 경우 ActiveRecord::RecordInvalid를 발생시킵니다.

collection.reload

collection.reload 메서드는 데이터베이스 읽기를 강제하여 연관된 모든 객체의 Relation을 반환합니다. 연관된 객체가 없는 경우 빈 Relation을 반환합니다.

@assemblies = @part.assemblies.reload

has_and_belongs_to_many에 대한 옵션

Rails는 대부분의 상황에서 잘 작동하는 지능적인 기본값을 사용하지만, has_and_belongs_to_many 연관관계 참조의 동작을 사용자 정의해야 하는 경우도 있습니다. 이러한 사용자 정의는 연관관계를 생성할 때 옵션을 전달하여 쉽게 수행할 수 있습니다. 예를 들어 이 연관관계는 두 가지 옵션을 사용합니다:

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { readonly },
                                       autosave: true
end

has_and_belongs_to_many 연관관계는 다음과 같은 옵션을 지원합니다:

  • :association_foreign_key
  • :autosave
  • :class_name
  • :foreign_key
  • :join_table
  • :strict_loading
  • :validate
:association_foreign_key

관례상 Rails는 조인 테이블의 열을 사용하여 다른 모델에 대한 외래 키를 보유한다고 가정합니다. 이 열 이름은 해당 모델 이름에 _id 접미사가 추가된 것입니다. :association_foreign_key 옵션을 사용하면 외래 키의 이름을 직접 설정할 수 있습니다.

팁: :foreign_key:association_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
:autosave

:autosave 옵션을 true로 설정하면 Rails는 로드된 연관 멤버를 저장하고 삭제 표시된 멤버를 삭제할 것입니다. :autosave 옵션을 false로 설정하는 것은 :autosave 옵션을 설정하지 않는 것과 같지 않습니다. :autosave 옵션이 없는 경우 새 연관 객체는 저장되지만 업데이트된 연관 객체는 저장되지 않습니다.

:class_name

다른 모델의 이름을 연관관계 이름에서 유추할 수 없는 경우 :class_name 옵션을 사용하여 모델 이름을 제공할 수 있습니다. 예를 들어 부품이 조립품을 가지고 있지만 실제 조립품 모델의 이름이 Gadget인 경우 다음과 같이 설정할 수 있습니다:

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
:foreign_key

관례상 Rails는 이 모델의 외래 키를 보유하는 조인 테이블의 열 이름이 이 모델 이름에 _id 접미사가 추가된 것이라고 가정합니다. :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
:join_table

기본 조인 테이블 이름이 어휘 순서에 따라 원하는 것이 아닌 경우 :join_table 옵션을 사용하여 기본값을 재정의할 수 있습니다.

:strict_loading

이 연관관계를 통해 연관된 레코드가 로드될 때마다 strict loading을 적용합니다.

:validate

:validate 옵션을 false로 설정하면 이 객체를 저장할 때 새 연관 객체가 유효성 검사되지 않습니다. 기본적으로 true입니다: 이 객체를 저장할 때 새 연관 객체가 유효성 검사됩니다.

has_and_belongs_to_many에 대한 범위

때때로 has_and_belongs_to_many에서 사용되는 쿼리를 사용자 정의하고 싶을 수 있습니다. 이러한 사용자 정의는 범위 블록을 통해 수행할 수 있습니다. 예를 들어:

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { where active: true }
end

표준 쿼리 메서드를 범위 블록 내에서 사용할 수 있습니다. 아래에서 설명하는 메서드는 다음과 같습니다:

  • where
  • extending
  • group
  • includes
  • limit
  • offset
  • order
  • readonly
  • select
  • distinct
where

where 메서드를 사용하면 연관된 객체가 충족해야 하는 조건을 지정할 수 있습니다.

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { where "factory = 'Seattle'" }
end

해시 스타일 where를 사용할 수도 있습니다:

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { where factory: 'Seattle' }
end

해시 스타일 where를 사용하면 이 연관관계를 통한 레코드 생성이 자동으로 해시 범위에 의해 제한됩니다. 이 경우 @parts.assemblies.create 또는 @parts.assemblies.build를 사용하면 factory 열이 "Seattle"인 조립품이 생성됩니다.

extending

extending 메서드는 연관관계 프록시를 확장할 명명된 모듈을 지정합니다. 연관관계 확장은 이 가이드의 뒷부분에서 자세히 다룹니다.

group

group 메서드는 결과 집합을 그룹화할 속성 이름을 제공합니다. SQL의 GROUP BY 절을 사용합니다.

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies, -> { group "factory" }
end
includes

includes 메서드를 사용하면 이 연관관계가 사용될 때 미리 로드해야 하는 두 번째 수준의 연관관계를 지정할 수 있습니다.

limit

limit 메서드를 사용하면 연관관계를 통해 가져올 객체 수를 제한할 수 있습니다.

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { order("created_at DESC").limit(50) }
end
offset

offset 메서드를 사용하면 연관관계를 통해 객체를 가져올 때 시작 오프셋을 지정할 수 있습니다. 예를 들어 offset(11)을 설정하면 처음 11개 레코드를 건너뜁니다.

order

order 메서드는 연관된 객체가 수신되는 순서를 결정합니다(SQL ORDER BY 절 구문 사용).

class Parts < ApplicationRecord
  has_and_belongs_to_many :assemblies,
    -> { order "assembly_name ASC" }
end
readonly

readonly를 사용하면 연관관계를 통해 검색된 연관된 객체는 읽기 전용이 됩니다.

select

select 메서드를 사용하면 연관된 객체에 대한 데이터를 검색하는 SQL SELECT 절을 재정의할 수 있습니다. 기본적으로 Rails는 모든 열을 검색합니다.

distinct

distinct 메서드를 사용하면 컬렉션에서 중복을 제거할 수 있습니다.

객체가 언제 저장되는가?

has_and_belongs_to_many 연관관계에 객체를 할당하면 해당 객체가 자동으로 저장됩니다(조인 테이블을 업데이트하기 위해). 한 문장에서 여러 객체를 할당하면 모두 저장됩니다.

이러한 저장 중 하나라도 유효성 검사 오류로 인해 실패하면 할당 문이 false를 반환하고 할당 자체가 취소됩니다.

부모 객체(has_and_belongs_to_many 연관관계를 선언한 객체)가 저장되지 않은 경우(new_record?true를 반환) 연관 객체는 추가될 때 저장되지 않습니다. 부모가 저장될 때 연관관계의 모든 저장되지 네, 계속해서 번역하겠습니다:

않은 멤버가 자동으로 저장됩니다.

has_and_belongs_to_many 연관관계에 객체를 할당하되 객체를 저장하지 않으려면 collection.build 메서드를 사용하세요.

연관관계 콜백

일반 콜백은 액티브 레코드 객체의 생명 주기에 연결되어 있어, 다양한 시점에서 해당 객체로 작업할 수 있습니다. 예를 들어 :before_save 콜백을 사용하여 객체가 저장되기 직전에 무언가를 발생시킬 수 있습니다.

연관관계 콜백은 일반 콜백과 유사하지만 컬렉션의 생명 주기 이벤트에 의해 트리거됩니다. 사용 가능한 연관관계 콜백은 다음과 같습니다:

  • before_add
  • after_add
  • before_remove
  • after_remove

연관관계 콜백은 연관관계 선언에 옵션을 추가하여 정의합니다. 예를 들어:

class Author < ApplicationRecord
  has_many :books, before_add: :check_credit_limit

  def check_credit_limit(book)
    # ...
  end
end

연관관계 콜백에 대한 자세한 내용은 액티브 레코드 콜백 가이드를 참조하세요.

연관관계 확장

연관관계 프록시 객체에 자동으로 빌드된 기능으로 제한되지 않습니다. 익명 모듈을 통해 이러한 객체를 확장하여 새 파인더, 생성자 또는 기타 메서드를 추가할 수도 있습니다. 예를 들어:

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

여러 연관관계에서 공유해야 하는 확장이 있는 경우 명명된 확장 모듈을 사용할 수 있습니다. 예를 들어:

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

연관관계 소유자를 사용한 연관관계 범위 지정

연관관계 소유자를 단일 인수로 범위 블록에 전달하면 연관관계 범위에 대해 더 많은 제어를 할 수 있습니다. 그러나 주의할 점은 연관관계 사전 로드가 더 이상 가능하지 않다는 것입니다.

class Supplier < ApplicationRecord
  has_one :account, ->(supplier) { where active: supplier.active? }
end

단일 테이블 상속(STI)

때로는 다른 모델 간에 필드와 동작을 공유하고 싶을 수 있습니다. 예를 들어 Car, Motorcycle, Bicycle 모델이 있다고 가정해 보겠습니다. colorprice 필드와 일부 메서드를 모두 공유하고 싶지만 각각의 고유한 동작과 별도의 컨트롤러도 필요합니다.

먼저 기본 Vehicle 모델을 생성해 보겠습니다:

$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}

"type” 필드를 추가했다는 것을 알아챘나요? 모든 모델이 단일 데이터베이스 테이블에 저장되므로 Rails는 이 열에 저장되는 모델 이름을 저장합니다. 이 예에서는 “Car”, “Motorcycle” 또는 “Bicycle"일 수 있습니다. STI가 작동하려면 테이블에 "type” 필드가 있어야 합니다.

다음으로 Vehicle에서 상속받는 Car 모델을 생성해 보겠습니다. 이를 위해 --parent=PARENT 옵션을 사용할 수 있습니다. 이 옵션은 지정된 부모에서 상속받는 모델을 생성하고 해당 마이그레이션은 생성하지 않습니다(테이블이 이미 존재하므로).

예를 들어 Car 모델을 생성하려면:

$ bin/rails generate model car --parent=Vehicle

생성된 모델은 다음과 같습니다:

class Car < Vehicle
end

이는 Vehicle에 추가된 모든 동작(연관관계, 공용 메서드 등)이 Car에서도 사용 가능함을 의미합니다.

자동차를 생성하면 vehicles 테이블에 “Car"로 type 필드가 저장됩니다:

Car.create(color: 'Red', price: 10000)

다음과 같은 SQL이 생성됩니다:

INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)

자동차 레코드를 쿼리하면 자동차만 검색됩니다:

Car.all

다음과 같은 쿼리가 실행됩니다:

SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')

상속 열 재정의

(레거시 데이터베이스와 작업할 때와 같이) 상속 열의 이름을 재정의해야 하는 경우가 있습니다. 이는 inheritance_column을 사용하여 달성할 수 있습니다.

# 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">

상속 열 비활성화

(레거시 데이터베이스와 작업할 때와 같이) 단일 테이블 상속을 완전히 비활성화해야 하는 경우가 있습니다. 그렇지 않으면 ActiveRecord::SubclassNotFound이 발생합니다.

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">

위임된 유형

단일 테이블 상속(STI)은 하위 클래스 간에 차이가 적고 모든 하위 클래스의 속성을 포함하는 단일 테이블을 만들 때 가장 잘 작동합니다.

이 접근 방식의 단점은 해당 테이블에 블로트가 발생한다는 것입니다. 다른 어떤 것에도 사용되지 않는 특정 하위 클래스의 속성까지 포함하게 됩니다.

다음 예에서는 "Entry” 클래스에서 상속받는 두 개의 Active Record 모델이 있습니다. 이 모델에는 subject 속성이 포함되어 있습니다.

# Schema: entries[ id, type, subject, created_at, updated_at]
class Entry < ApplicationRecord
end

class Comment < Entry
end

class Message < Entry
end

위임된 유형은 이 문제를 delegated_type를 통해 해결합니다.

위임된 유형을 사용하려면 데이터를 특정 방식으로 모델링해야 합니다. 요구 사항은 다음과 같습니다:

  • 공유 속성을 저장하는 수퍼클래스가 있습니다.
  • 각 하위 클래스는 수퍼클래스에서 상속받아야 하며 추가 속성에 대한 별도의 테이블이 있어야 합니다.

이렇게 하면 모든 하위 클래스에 공통적으로 정의된 속성을 단일 테이블에 정의할 필요가 없습니다.

위의 예를 적용하려면 모델을 다시 생성해야 합니다. 먼저 수퍼클래스 Entry 모델을 생성해 보겠습니다:

$ bin/rails generate model entry entryable_type:string entryable_id:integer

그런 다음 위임을 위한 새 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

delegated_type 선언

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

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

entryable 매개변수는 위임에 사용할 필드를 지정하며 MessageComment를 위임 클래스로 포함합니다.

Entry 클래스에는 entryable_typeentryable_id 필드가 있습니다. 이는 delegated_type 정의에서 entryable 이름에 _type, _id 접미사가 추가된 필드입니다. entryable_type은 위임 대상의 하위 클래스 이름을 저장하고 entryable_id는 위임 대상 하위 클래스 레코드의 ID를 저장합니다.

다음으로 as: :entryable 매개변수를 사용하여 위임된 유형을 구현하는 모듈을 정의해야 합니다.

module Entryable
  extend ActiveSupport::Concern

  included do
    has_one :entry, as: :entryable, touch: true
  end
end

그런 다음 생성된 모듈을 하위 클래스에 포함시킵니다.

class Message < ApplicationRecord
  include Entryable
end

class Comment < ApplicationRecord
  include Entryable
end

이 정의가 완료되면 Entry 위임자가 다음과 같은 메서드를 제공합니다:

메서드 반환
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"일 때 메시지 레코드 반환, 그렇지 않으면 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"일 때 댓글 레코드 반환, 그렇지 않으면 nil
Entry#comment_id entryable_type == "Comment"일 때 entryable_id 반환, 그렇지 않으면 nil

객체 생성

Entry 객체를 생성할 때 entryable 하위 클래스를 동시에 지정할 수 있습니다.

Entry.create! entryable: Message.new(subject: "hello!")

추가 위임 추가

Entry 위임자를 확장하고 다형성을 사용하여 하위 클래스에서 추가 기능을 정의할 수 있습니다. 예를 들어 Entry에서 title 메서드를 하위 클래스에 위임하려면:

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