Active Record와 PostgreSQL
이 가이드는 Active Record에서 PostgreSQL 특정 사용법을 다룹니다.
이 가이드를 읽고 나면 다음을 알 수 있습니다:
- PostgreSQL의 데이터 유형을 사용하는 방법.
- UUID 기본 키를 사용하는 방법.
- 키가 아닌 열을 인덱스에 포함하는 방법.
- 연기 가능한 외래 키를 사용하는 방법.
- 고유 제약 조건을 사용하는 방법.
- 제외 제약 조건을 구현하는 방법.
- PostgreSQL로 전체 텍스트 검색을 구현하는 방법.
- Active Record 모델을 데이터베이스 뷰로 지원하는 방법.
PostgreSQL 어댑터를 사용하려면 최소 9.3 버전이 설치되어 있어야 합니다. 이전 버전은 지원되지 않습니다.
PostgreSQL 시작하기 위해서는 Rails 구성 가이드를 참고하세요. Active Record에서 PostgreSQL을 올바르게 설정하는 방법이 설명되어 있습니다.
데이터 유형
PostgreSQL은 다양한 특정 데이터 유형을 제공합니다. 다음은 PostgreSQL 어댑터에서 지원되는 유형 목록입니다.
Bytea
# db/migrate/20140207133952_create_documents.rb create_table :documents do |t| t.binary 'payload' end
# app/models/document.rb class Document < ApplicationRecord end
# Usage data = File.read(Rails.root + "tmp/output.pdf") Document.create payload: data
Array
# db/migrate/20140207133952_create_books.rb create_table :books do |t| t.string 'title' t.string 'tags', array: true t.integer 'ratings', array: true end add_index :books, :tags, using: 'gin' add_index :books, :ratings, using: 'gin'
# app/models/book.rb class Book < ApplicationRecord end
# Usage Book.create title: "Brave New World", tags: ["fantasy", "fiction"], ratings: [4, 5] ## 단일 태그에 대한 도서 Book.where("'fantasy' = ANY (tags)") ## 다중 태그에 대한 도서 Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"]) ## 3개 이상의 평점이 있는 도서 Book.where("array_length(ratings, 1) >= 3")
Hstore
참고: hstore를 사용하려면 hstore
확장을 활성화해야 합니다.
# db/migrate/20131009135255_create_profiles.rb class CreateProfiles < ActiveRecord::Migration[7.0] enable_extension 'hstore' unless extension_enabled?('hstore') create_table :profiles do |t| t.hstore 'settings' end end
# app/models/profile.rb class Profile < ApplicationRecord end
irb> Profile.create(settings: { "color" => "blue", "resolution" => "800x600" }) irb> profile = Profile.first irb> profile.settings => {"color"=>"blue", "resolution"=>"800x600"} irb> profile.settings = {"color" => "yellow", "resolution" => "1280x1024"} irb> profile.save! irb> Profile.where("settings->'color' = ?", "yellow") => #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
JSON과 JSONB
# db/migrate/20131220144913_create_events.rb # ... json 데이터 유형의 경우: create_table :events do |t| t.json 'payload' end # ... 또는 jsonb 데이터 유형의 경우: create_table :events do |t| t.jsonb 'payload' end
# app/models/event.rb class Event < ApplicationRecord end
irb> Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]}) irb> event = Event.first irb> event.payload => {"kind"=>"user_renamed", "change"=>["jack", "john"]} ## JSON 문서 기반 쿼리 # -> 연산자는 원래 JSON 유형(객체일 수 있음)을 반환하고, ->>는 텍스트를 반환합니다. irb> Event.where("payload->>'kind' = ?", "user_renamed")
범위 유형
이 유형은 Ruby Range
객체에 매핑됩니다.
# db/migrate/20130923065404_create_events.rb create_table :events do |t| t.daterange 'duration' end
# app/models/event.rb class Event < ApplicationRecord end
irb> Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12)) irb> event = Event.first irb> event.duration => Tue, 11 Feb 2014...Thu, 13 Feb 2014 ## 특정 날짜에 대한 모든 이벤트 irb> Event.where("duration @> ?::date", Date.new(2014, 2, 12)) ## 범위 경계 작업 irb> event = Event.select("lower(duration) AS starts_at").select("upper(duration) AS ends_at").first irb> event.starts_at => Tue, 11 Feb 2014 irb> event.ends_at => Thu, 13 Feb 2014
복합 유형
현재 복합 유형에 대한 특별한 지원은 없습니다. 일반 텍스트 열에 매핑됩니다:
CREATE TYPE full_address AS ( city VARCHAR(90), street VARCHAR(90) );
# db/migrate/20140207133952_create_contacts.rb execute <<-SQL CREATE TYPE full_address AS ( city VARCHAR(90), street VARCHAR(90) ); SQL create_table :contacts do |t| t.column :address, :full_address end
# app/models/contact.rb class Contact < ApplicationRecord end
irb> Contact.create address: "(Paris,Champs-Élysées)" irb> contact = Contact.first irb> contact.address => "(Paris,Champs-Élysées)" irb> contact.address = "(Paris,Rue Basse)" irb> contact.save!
열거형 유형
이 유형은 일반 텍스트 열로 매핑하거나 ActiveRecord::Enum
에 매핑할 수 있습니다.
# db/migrate/20131220144913_create_articles.rb def change create_enum :article_status, ["draft", "published", "archived"] create_table :articles do |t| t.enum :status, enum_type: :article_status, default: "draft", null: false end end
기존 테이블에 열거형 열을 추가할 수도 있습니다:
# db/migrate/20230113024409_add_status_to_articles.rb def change create_enum :article_status, ["draft", "published", "archived"] add_column :articles, :status, :enum, enum_type: :article_status, default: "draft", null: false end
위의 마이그레이션은 모두 되돌릴 수 있지만, 필요한 경우 별도의 #up
및 #down
메서드를 정의할 수 있습니다. 열거형 유형을 삭제하기 전에 해당 유형에 의존하는 열이나 테이블을 제거해야 합니다:
def down drop_table :articles # 또는: remove_column :articles, :status drop_enum :article_status end
모델에서 열거형 속성을 선언하면 헬퍼 메서드가 추가되고 클래스의 인스턴스에 잘못된 값이 할당되는 것을 방지합니다:
# app/models/article.rb class Article < ApplicationRecord enum :status, { draft: "draft", published: "published", archived: "archived" }, prefix: true end
irb> article = Article.create irb> article.status => "draft" # PostgreSQL에서 정의된 기본 상태 irb> article.status_published! irb> article.status => "published" irb> article.status_archived? => false irb> article.status = "deleted" ArgumentError: 'deleted' is not a valid status
열거형 이름을 변경하려면 rename_enum
을 사용하고 모델 사용을 업데이트해야 합니다:
# db/migrate/20150718144917_rename_article_status.rb def change rename_enum :article_status, to: :article_state end
새 값을 추가하려면 add_enum_value
를 사용할 수 있습니다:
# db/migrate/20150720144913_add_new_state_to_articles.rb def up add_enum_value :article_state, "archived" # published 다음에 추가됨 add_enum_value :article_state, "in review", before: "published" add_enum_value :article_state, "approved", after: "in review" end
참고: 열거형 값은 삭제할 수 없으므로 addenumvalue는 되돌릴 수 없습니다. 이유는 여기에서 확인할 수 있습니다.
값 이름을 변경하려면 rename_enum_value
를 사용할 수 있습니다:
# db/migrate/20150722144915_rename_article_state.rb def change rename_enum_value :article_state, from: "archived", to: "deleted" end
힌트: 모든 열거형의 모든 값을 표시하려면 bin/rails db
또는 psql
콘솔에서 다음 쿼리를 실행할 수 있습니다:
SELECT n.nspname AS enum_schema, t.typname AS enum_name, e.enumlabel AS enum_value FROM pg_type t JOIN pg_enum e ON t.oid = e.enumtypid JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
UUID
참고: PostgreSQL 버전이 13.0 미만인 경우 UUID를 사용하려면 특별한 확장을 활성화해야 할 수 있습니다. pgcrypto
확장(PostgreSQL >= 9.4) 또는 uuid-ossp
확장(이전 릴리스)을 활성화하세요.
# db/migrate/20131220144913_create_revisions.rb create_table :revisions do |t| t.uuid :identifier end
# app/models/revision.rb class Revision < ApplicationRecord end
irb> Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11" irb> revision = Revision.first irb> revision.identifier => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
마이그레이션에서 uuid
유형을 사용하여 참조를 정의할 수 있습니다:
# db/migrate/20150418012400_create_blog.rb enable_extension 'pgcry네, 번역을 계속하겠습니다. ### UUID 기본 키 ```ruby # db/migrate/20150418012400_create_blog.rb enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') create_table :posts, id: :uuid create_table :comments, id: :uuid do |t| # t.belongs_to :post, type: :uuid t.references :post, type: :uuid end
# app/models/post.rb class Post < ApplicationRecord has_many :comments end
# app/models/comment.rb class Comment < ApplicationRecord belongs_to :post end
이 섹션에서 UUID를 기본 키로 사용하는 방법에 대해 자세히 설명합니다.
비트 문자열 유형
# db/migrate/20131220144913_create_users.rb create_table :users, force: true do |t| t.column :settings, "bit(8)" end
# app/models/user.rb class User < ApplicationRecord end
irb> User.create settings: "01010011" irb> user = User.first irb> user.settings => "01010011" irb> user.settings = "0xAF" irb> user.settings => "10101111" irb> user.save!
네트워크 주소 유형
inet
및 cidr
유형은 Ruby IPAddr
객체에 매핑됩니다. macaddr
유형은 일반 텍스트에 매핑됩니다.
# db/migrate/20140508144913_create_devices.rb create_table(:devices, force: true) do |t| t.inet 'ip' t.cidr 'network' t.macaddr 'address' end
# app/models/device.rb class Device < ApplicationRecord end
irb> macbook = Device.create(ip: "192.168.1.12", network: "192.168.2.0/24", address: "32:01:16:6d:05:ef") irb> macbook.ip => #<IPAddr: IPv4:192.168.1.12/255.255.255.255> irb> macbook.network => #<IPAddr: IPv4:192.168.2.0/255.255.255.0> irb> macbook.address => "32:01:16:6d:05:ef"
기하학적 유형
점을 제외한 모든 기하학적 유형은 일반 텍스트에 매핑됩니다. 점은 x
및 y
좌표를 포함하는 배열로 캐스팅됩니다.
간격
이 유형은 ActiveSupport::Duration
객체에 매핑됩니다.
# db/migrate/20200120000000_create_events.rb create_table :events do |t| t.interval 'duration' end
# app/models/event.rb class Event < ApplicationRecord end
irb> Event.create(duration: 2.days) irb> event = Event.first irb> event.duration => 2 days
UUID 기본 키
참고: pgcrypto
(PostgreSQL >= 9.4) 또는 uuid-ossp
확장을 활성화해야 무작위 UUID를 생성할 수 있습니다.
# db/migrate/20131220144913_create_devices.rb enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') create_table :devices, id: :uuid do |t| t.string :kind end
# app/models/device.rb class Device < ApplicationRecord end
irb> device = Device.create irb> device.id => "814865cd-5a1d-4771-9306-4268f188fe9e"
참고: :default
옵션을 create_table
에 전달하지 않으면 gen_random_uuid()
(from pgcrypto
)가 가정됩니다.
UUID를 기본 키로 사용하는 테이블에 대한 Rails 모델 생성기를 사용하려면 --primary-key-type=uuid
를 전달하세요.
예를 들어:
$ rails generate model Device --primary-key-type=uuid kind:string
UUID를 참조하는 외래 키가 있는 모델을 빌드할 때는 uuid
를 네이티브 필드 유형으로 처리하세요. 예:
$ rails generate model Case device_id:uuid
인덱싱
PostgreSQL에는 다양한 인덱스 옵션이 포함되어 있습니다. 다음 옵션은 일반적인 인덱스 옵션에 추가로 PostgreSQL 어댑터에서 지원됩니다.
포함
새 인덱스를 만들 때 :include
옵션을 사용하여 키가 아닌 열을 포함할 수 있습니다. 이 키는 검색을 위한 인덱스 스캔에 사용되지 않지만 관련 테이블을 방문하지 않고도 인덱스 전용 스캔 중에 읽을 수 있습니다.
# db/migrate/20131220144913_add_index_users_on_email_include_id.rb add_index :users, :email, include: :id
여러 열을 지원합니다:
# db/migrate/20131220144913_add_index_users_on_email_include_id_and_created_at.rb add_index :users, :email, include: [:id, :created_at]
생성된 열
참고: 생성된 열은 PostgreSQL 12.0 버전부터 지원됩니다.
# db/migrate/20131220144913_create_users.rb create_table :users do |t| t.string :name t.virtual :name_upcased, type: :string, as: 'upper(name)', stored: true end # app/models/user.rb class User < ApplicationRecord end # Usage user = User.create(name: 'John') User.last.name_upcased # => "JOHN"
연기 가능한 외래 키
기본적으로 PostgreSQL의 테이블 제약 조건은 각 문장 직후에 즉시 확인됩니다. 참조된 레코드가 아직 참조 테이블에 없는 경우 레코드를 만들지 않습니다. 트랜잭션이 커밋될 때 무결성 검사를 나중에 실행하도록 DEFERRABLE
을 외래 키 정의에 추가할 수 있습니다. 기본적으로 모든 검사를 연기하려면 DEFERRABLE INITIALLY DEFERRED
로 설정할 수 있습니다. Rails는 add_reference
및 add_foreign_key
메서드의 :deferrable
키를 추가하여 이 PostgreSQL 기능을 노출합니다.
순환 종속성을 트랜잭션에서 만드는 한 예는 다음과 같습니다:
add_reference :person, :alias, foreign_key: { deferrable: :deferred } add_reference :alias, :person, foreign_key: { deferrable: :deferred }
참조가 foreign_key: true
옵션으로 생성된 경우 첫 번째 INSERT
문을 실행할 때 다음 트랜잭션이 실패합니다. 그러나 deferrable: :deferred
옵션이 설정되면 실패하지 않습니다.
ActiveRecord::Base.connection.transaction do person = Person.create(id: SecureRandom.uuid, alias_id: SecureRandom.uuid, name: "John Doe") Alias.create(id: person.alias_id, person_id: person.id, name: "jaydee") end
:deferrable
옵션이 :immediate
로 설정되면 기본 동작을 유지하고 set_constraints
내에서 수동으로 검사를 연기할 수 있습니다. 이렇게 하면 트랜잭션이 커밋될 때 외래 키가 확인됩니다:
ActiveRecord::Base.connection.transaction do ActiveRecord::Base.connection.set_constraints(:deferred) person = Person.create(alias_id: SecureRandom.uuid, name: "John Doe") Alias.create(id: person.alias_id, person_id: person.id, name: "jaydee") end
기본적으로 :deferrable
은 false
이며 제약 조건은 항상 즉시 확인됩니다.
고유 제약 조건
# db/migrate/20230422225213_create_items.rb create_table :items do |t| t.integer :position, null: false t.unique_constraint [:position], deferrable: :immediate end
기존 고유 인덱스를 연기 가능하게 변경하려면 :using_index
를 사용하여 연기 가능한 고유 제약 조건을 만들 수 있습니다.
add_unique_constraint :items, deferrable: :deferred, using_index: "index_items_on_position"
외래 키와 마찬가지로 :deferrable
을 :immediate
또는 :deferred
로 설정하여 고유 제약 조건을 연기할 수 있습니다. 기본적으로 :deferrable
은 false
이며 제약 조건은 항상 즉시 확인됩니다.
제외 제약 조건
# db/migrate/20131220144913_create_products.rb create_table :products do |t| t.integer :price, null: false t.daterange :availability_range, null: false t.exclusion_constraint "price WITH =, availability_range WITH &&", using: :gist, name: "price_check" end
외래 키와 마찬가지로 :deferrable
을 :immediate
또는 :deferred
로 설정하여 제외 제약 조건을 연기할 수 있습니다. 기본적으로 :deferrable
은 false
이며 제약 조건은 항상 즉시 확인됩니다.
전체 텍스트 검색
# db/migrate/20131220144913_create_documents.rb create_table :documents do |t| t.string :title t.string :body end add_index :documents, "to_tsvector('english', title || ' ' || body)", using: :gin, name: 'documents_idx'
# app/models/document.rb class Document < ApplicationRecord end
# Usage Document.create(title: "Cats and Dogs", body: "are nice!") ## 'cat & dog'와 일치하는 모든 문서 Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)", "cat & dog")
선택적으로 벡터를 자동으로 생성된 열(PostgreSQL 12.0 이상)에 저장할 수 있습니다:
# db/migrate/20131220144913_create_documents.rb create_table :documents do |t| t.string :title t.string :body t.virtual :textsearchable_index_col, type: :tsvector, as: "to_tsvector('english', title || ' ' || body)", stored: true end add_index :documents, :textsearchable_index_col, using: :gin, name: 'documents_idx' # Usage Document.create(title: "Cats and Dogs", body: "are nice!") ## 'cat & dog'와 일치하는 모든 문서 Document.where("textsearchable_index_col @@ to_tsquery(?)", "cat & dog")
데이터베이스 뷰
다음과 같은 테이블이 포함된 레거시 데이터베이스로 작업해야 한다고 가정해 보겠습니다:
rails_pg_guide=# \d "TBL_ART" Table "public.TBL_ART" Column | Type | 네, 번역을 계속하겠습니다. 데이터베이스 뷰 -------------- * [뷰 생성](https://www.postgresql.org/docs/current/static/sql-createview.html) 다음과 같은 테이블이 포함된 레거시 데이터베이스로 작업해야 한다고 가정해 보겠습니다:
railspgguide=# \d “TBLART” Table “public.TBLART” Column | Type | Modifiers ————+—————————–+———————————————————— INTID | integer | not null default nextval(‘“TBLARTINTIDseq”’::regclass) STRTITLE | character varying | STRSTAT | character varying | default ‘draft’::character varying DTPUBLAT | timestamp without time zone | BLARCH | boolean | default false Indexes: “TBLARTpkey” PRIMARY KEY, btree (“INT_ID”) “`
이 테이블은 Rails 규칙을 전혀 따르지 않습니다. PostgreSQL 뷰는 기본적으로 업데이트 가능하므로 다음과 같이 래핑할 수 있습니다:
# db/migrate/20131220144913_create_articles_view.rb execute <<-SQL CREATE VIEW articles AS SELECT "INT_ID" AS id, "STR_TITLE" AS title, "STR_STAT" AS status, "DT_PUBL_AT" AS published_at, "BL_ARCH" AS archived FROM "TBL_ART" WHERE "BL_ARCH" = 'f' SQL
# app/models/article.rb class Article < ApplicationRecord self.primary_key = "id" def archive! update_attribute :archived, true end end
irb> first = Article.create! title: "Winter is coming", status: "published", published_at: 1.year.ago irb> second = Article.create! title: "Brace yourself", status: "draft", published_at: 1.month.ago irb> Article.count => 2 irb> first.archive! irb> Article.count => 1
참고: 이 애플리케이션은 비보관 Articles
만 관심이 있습니다. 뷰를 사용하면 보관된 Articles
를 직접 제외할 수 있습니다.
구조 덤프
config.active_record.schema_format
이 :sql
인 경우 Rails는 pg_dump
를 호출하여 구조 덤프를 생성합니다.
ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags
를 사용하여 pg_dump
를 구성할 수 있습니다.
예를 들어, 구조 덤프에서 주석을 제외하려면 이니셜라이저에 다음을 추가하세요:
ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ["--no-comments"]