edge
Más en rubyonrails.org: Más Ruby on Rails

Active Record y PostgreSQL

Esta guía cubre el uso específico de Active Record con PostgreSQL.

Después de leer esta guía, sabrás:

Para usar el adaptador de PostgreSQL, necesitas tener instalada al menos la versión 9.3. Las versiones anteriores no son compatibles.

Para comenzar con PostgreSQL, consulta la guía de configuración de Rails. Describe cómo configurar correctamente Active Record para PostgreSQL.

1 Tipos de datos

PostgreSQL ofrece una serie de tipos de datos específicos. A continuación se muestra una lista de los tipos que son compatibles con el adaptador de PostgreSQL.

1.1 Bytea

# db/migrate/20140207133952_create_documents.rb
create_table :documents do |t|
  t.binary 'payload'
end
# app/models/document.rb
class Document < ApplicationRecord
end
# Uso
data = File.read(Rails.root + "tmp/output.pdf")
Document.create payload: data

1.2 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
# Uso
Book.create title: "Brave New World",
            tags: ["fantasy", "fiction"],
            ratings: [4, 5]

## Libros para una sola etiqueta
Book.where("'fantasy' = ANY (tags)")

## Libros para varias etiquetas
Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])

## Libros con 3 o más calificaciones
Book.where("array_length(ratings, 1) >= 3")

1.3 Hstore

NOTA: Necesitas habilitar la extensión hstore para usar 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"}>]>

1.4 JSON y JSONB

# db/migrate/20131220144913_create_events.rb
# ... para el tipo de dato json:
create_table :events do |t|
  t.json 'payload'
end
# ... o para el tipo de dato 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"]}

## Consulta basada en el documento JSON
# El operador -> devuelve el tipo JSON original (que puede ser un objeto), mientras que ->> devuelve texto
irb> Event.where("payload->>'kind' = ?", "user_renamed")

1.5 Tipos de rango

Este tipo se asigna a objetos 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

## Todos los eventos en una fecha determinada
irb> Event.where("duration @> ?::date", Date.new(2014, 2, 12))

## Trabajando con límites de rango
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

1.6 Tipos compuestos

Actualmente no hay soporte especial para tipos compuestos. Se asignan a columnas de texto normales:

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!

1.7 Tipos enumerados

El tipo se puede asignar como una columna de texto normal o a un 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

También puedes crear un tipo de enumeración y agregar una columna de enumeración a una tabla existente:

# 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

Las migraciones anteriores son reversibles, pero puedes definir métodos separados #up y #down si es necesario. Asegúrate de eliminar cualquier columna o tabla que dependa del tipo de enumeración antes de eliminarlo:

def down
  drop_table :articles

  # O: remove_column :articles, :status
  drop_enum :article_status
end

Declarar un atributo de enumeración en el modelo agrega métodos auxiliares y evita que se asignen valores no válidos a las instancias de la clase:

# 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" # estado predeterminado de PostgreSQL, como se define en la migración anterior

irb> article.status_published!
irb> article.status
=> "published"

irb> article.status_archived?
=> false

irb> article.status = "deleted"
ArgumentError: 'deleted' no es un estado válido

Para cambiar el nombre de la enumeración, puedes usar rename_enum junto con la actualización de cualquier uso del modelo:

# db/migrate/20150718144917_rename_article_status.rb
def change
  rename_enum :article_status, to: :article_state
end

Para agregar un nuevo valor, puedes usar add_enum_value:

# db/migrate/20150720144913_add_new_state_to_articles.rb
def up
  add_enum_value :article_state, "archived", # estará al final después de publicado
  add_enum_value :article_state, "in review", before: "published"
  add_enum_value :article_state, "approved", after: "in review"
end

NOTA: Los valores de enumeración no se pueden eliminar, lo que también significa que add_enum_value no se puede revertir. Puedes leer por qué aquí.

Para cambiar el nombre de un valor, puedes usar rename_enum_value:

# db/migrate/20150722144915_rename_article_state.rb
def change
  rename_enum_value :article_state, from: "archived", to: "deleted"
end

Sugerencia: para mostrar todos los valores de todas las enumeraciones que tienes, puedes llamar a esta consulta en la consola bin/rails db o 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

1.8 UUID

NOTA: Si estás utilizando PostgreSQL anterior a la versión 13.0, es posible que debas habilitar extensiones especiales para usar UUID. Habilita la extensión pgcrypto (PostgreSQL >= 9.4) o la extensión uuid-ossp (para versiones anteriores).

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

Puedes usar el tipo uuid para definir referencias en migraciones:

# 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

Consulta esta sección para obtener más detalles sobre cómo usar UUID como clave primaria.

1.9 Tipos de cadena de bits

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

1.10 Tipos de dirección de red

Los tipos inet y cidr se asignan a objetos Ruby IPAddr. El tipo macaddr se asigna a texto normal.

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

1.11 Tipos geométricos

Todos los tipos geométricos, con la excepción de los puntos, se asignan a texto normal. Un punto se convierte en una matriz que contiene las coordenadas x e y.

1.12 Intervalo

Este tipo se asigna a objetos 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

2 Claves primarias UUID

NOTA: Debes habilitar la extensión pgcrypto (solo PostgreSQL >= 9.4) o uuid-ossp para generar UUID aleatorios. ```ruby

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"

NOTA: Se asume que se utiliza gen_random_uuid() (de pgcrypto) si no se pasa la opción :default a create_table.

Para usar el generador de modelos de Rails para una tabla que utiliza UUID como clave primaria, se debe pasar --primary-key-type=uuid al generador de modelos.

Por ejemplo:

$ rails generate model Device --primary-key-type=uuid kind:string

Al construir un modelo con una clave externa que referencia a este UUID, se debe tratar uuid como el tipo de campo nativo, por ejemplo:

$ rails generate model Case device_id:uuid

3 Indexación

PostgreSQL incluye una variedad de opciones de índice. Las siguientes opciones son compatibles con el adaptador de PostgreSQL además de las opciones de índice comunes.

3.1 Include

Al crear un nuevo índice, se pueden incluir columnas que no son clave con la opción :include. Estas claves no se utilizan en las exploraciones de índice para la búsqueda, pero se pueden leer durante una exploración solo de índice sin tener que visitar la tabla asociada.

# db/migrate/20131220144913_add_index_users_on_email_include_id.rb

add_index :users, :email, include: :id

Se admiten múltiples columnas:

# db/migrate/20131220144913_add_index_users_on_email_include_id_and_created_at.rb

add_index :users, :email, include: [:id, :created_at]

4 Columnas Generadas

NOTA: Las columnas generadas son compatibles desde la versión 12.0 de PostgreSQL.

# 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

# Uso
user = User.create(name: 'John')
User.last.name_upcased # => "JOHN"

5 Claves Externas Deferibles

Por defecto, las restricciones de tabla en PostgreSQL se verifican inmediatamente después de cada declaración. No permite intencionalmente crear registros donde el registro referenciado aún no está en la tabla referenciada. Sin embargo, es posible ejecutar esta verificación de integridad más adelante cuando la transacción se confirma agregando DEFERRABLE a la definición de la clave externa. Para diferir todas las verificaciones de forma predeterminada, se puede establecer en DEFERRABLE INITIALLY DEFERRED. Rails expone esta característica de PostgreSQL agregando la clave :deferrable a las opciones de foreign_key en los métodos add_reference y add_foreign_key.

Un ejemplo de esto es crear dependencias circulares en una transacción incluso si se han creado claves externas:

add_reference :person, :alias, foreign_key: { deferrable: :deferred }
add_reference :alias, :person, foreign_key: { deferrable: :deferred }

Si la referencia se creó con la opción foreign_key: true, la siguiente transacción fallaría al ejecutar la primera instrucción INSERT. Sin embargo, no falla cuando se establece la opción 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

Cuando la opción :deferrable se establece en :immediate, permite que las claves externas mantengan el comportamiento predeterminado de verificar la restricción inmediatamente, pero permite diferir manualmente las verificaciones utilizando SET CONSTRAINTS ALL DEFERRED dentro de una transacción. Esto hará que las claves externas se verifiquen cuando se confirme la transacción:

ActiveRecord::Base.transaction do
  ActiveRecord::Base.connection.execute("SET CONSTRAINTS ALL DEFERRED")
  person = Person.create(alias_id: SecureRandom.uuid, name: "John Doe")
  Alias.create(id: person.alias_id, person_id: person.id, name: "jaydee")
end

Por defecto, :deferrable es false y la restricción siempre se verifica inmediatamente.

6 Restricción Única

# db/migrate/20230422225213_create_items.rb
create_table :items do |t|
  t.integer :position, null: false
  t.unique_key [:position], deferrable: :immediate
end

Si se desea cambiar un índice único existente a diferible, se puede utilizar :using_index para crear restricciones únicas diferibles.

add_unique_key :items, deferrable: :deferred, using_index: "index_items_on_position"

Al igual que las claves externas, las restricciones únicas se pueden diferir estableciendo :deferrable en :immediate o :deferred. Por defecto, :deferrable es false y la restricción siempre se verifica inmediatamente.

7 Restricciones de Exclusión

# 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

Al igual que las claves externas, las restricciones de exclusión se pueden diferir estableciendo :deferrable en :immediate o :deferred. Por defecto, :deferrable es false y la restricción siempre se verifica inmediatamente.

8 Búsqueda de Texto Completo

# 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
# Uso
Document.create(title: "Gatos y Perros", body: "son lindos!")

## todos los documentos que coinciden con 'gato y perro'
Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
                 "gato & perro")

Opcionalmente, puedes almacenar el vector como una columna generada automáticamente (a partir de 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'

# Uso
Document.create(title: "Gatos y Perros", body: "son lindos!")

## todos los documentos que coinciden con 'gato y perro'
Document.where("textsearchable_index_col @@ to_tsquery(?)", "gato & perro")

9 Vistas de base de datos

Imagina que necesitas trabajar con una base de datos heredada que contiene la siguiente tabla:

rails_pg_guide=# \d "TBL_ART"
                                        Tabla "public.TBL_ART"
   Columna   |            Tipo             |                         Modificadores
------------+-----------------------------+------------------------------------------------------------
 INT_ID     | integer                     | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
 STR_TITLE  | character varying           |
 STR_STAT   | character varying           | default 'draft'::character varying
 DT_PUBL_AT | timestamp without time zone |
 BL_ARCH    | boolean                     | default false
Índices:
    "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")

Esta tabla no sigue las convenciones de Rails en absoluto. Debido a que las vistas simples de PostgreSQL son actualizables por defecto, podemos envolverla de la siguiente manera:

# 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: "Se acerca el invierno", status: "publicado", published_at: 1.año.ago
irb> second = Article.create! title: "Prepárate", status: "borrador", published_at: 1.mes.ago

irb> Article.count
=> 2
irb> first.archive!
irb> Article.count
=> 1

NOTA: Esta aplicación solo se preocupa por los Artículos no archivados. Una vista también permite condiciones para excluir directamente los Artículos archivados.

10 Volcados de estructura

Si tu config.active_record.schema_format es :sql, Rails llamará a pg_dump para generar un volcado de estructura.

Puedes usar ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags para configurar pg_dump. Por ejemplo, para excluir comentarios de tu volcado de estructura, agrega esto a un inicializador:

ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ['--no-comments']

Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor, contribuye si encuentras algún error tipográfico o factual. Para empezar, puedes leer nuestra contribución a la documentación sección.

También puedes encontrar contenido incompleto o desactualizado. Por favor, añade cualquier documentación faltante para main. Asegúrate de revisar Edge Guides primero para verificar si los problemas ya están resueltos o no en la rama principal. Consulta las Directrices de las Guías de Ruby on Rails para el estilo y las convenciones.

Si por alguna razón encuentras algo que corregir pero no puedes solucionarlo tú mismo, por favor abre un problema.

Y por último, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.