Claves Primarias Compuestas

Esta guía es una introducción a las claves primarias compuestas para las tablas de bases de datos.

Después de leer esta guía podrás:


1 ¿Qué son las Claves Primarias Compuestas?

A veces el valor de una sola columna no es suficiente para identificar de manera única cada fila de una tabla, y se requiere una combinación de dos o más columnas. Esto puede ser el caso cuando se utiliza un esquema de base de datos heredado sin una sola columna id como clave primaria, o al alterar esquemas para particionamiento o multi-tenencia.

Las claves primarias compuestas aumentan la complejidad y pueden ser más lentas que una sola columna de clave primaria. Asegúrate de que tu caso de uso requiera una clave primaria compuesta antes de usar una.

2 Migraciones de Claves Primarias Compuestas

Puedes crear una tabla con una clave primaria compuesta pasando la opción :primary_key a create_table con un valor de array:

class CreateProducts < ActiveRecord::Migration[7.2]
  def change
    create_table :products, primary_key: [:store_id, :sku] do |t|
      t.integer :store_id
      t.string :sku
      t.text :description
    end
  end
end

3 Consultando Modelos

3.1 Usando #find

Si tu tabla usa una clave primaria compuesta, necesitarás pasar un array cuando uses #find para localizar un registro:

# Encuentra el producto con store_id 3 y sku "XYZ12345"
irb> product = Product.find([3, "XYZ12345"])
=> #<Product store_id: 3, sku: "XYZ12345", description: "Yellow socks">

El equivalente SQL de lo anterior es:

SELECT * FROM products WHERE store_id = 3 AND sku = "XYZ12345"

Para encontrar múltiples registros con IDs compuestos, pasa un array de arrays a #find:

# Encuentra los productos con claves primarias [1, "ABC98765"] y [7, "ZZZ11111"]
irb> products = Product.find([[1, "ABC98765"], [7, "ZZZ11111"]])
=> [
  #<Product store_id: 1, sku: "ABC98765", description: "Red Hat">,
  #<Product store_id: 7, sku: "ZZZ11111", description: "Green Pants">
]

El equivalente SQL de lo anterior es:

SELECT * FROM products WHERE (store_id = 1 AND sku = 'ABC98765' OR store_id = 7 AND sku = 'ZZZ11111')

Los modelos con claves primarias compuestas también usarán la clave primaria compuesta completa al ordenar:

irb> product = Product.first
=> #<Product store_id: 1, sku: "ABC98765", description: "Red Hat">

El equivalente SQL de lo anterior es:

SELECT * FROM products ORDER BY products.store_id ASC, products.sku ASC LIMIT 1

3.2 Usando #where

Las condiciones de hash para #where pueden especificarse en una sintaxis similar a una tupla. Esto puede ser útil para consultar relaciones de claves primarias compuestas:

Product.where(Product.primary_key => [[1, "ABC98765"], [7, "ZZZ11111"]])

3.2.1 Condiciones con :id

Al especificar condiciones en métodos como find_by y where, el uso de id coincidirá con un atributo :id en el modelo. Esto es diferente de find, donde el ID pasado debe ser un valor de clave primaria.

Ten cuidado al usar find_by(id:) en modelos donde :id no es la clave primaria, como en modelos de clave primaria compuesta. Consulta la guía de Consultas de Active Record para aprender más.

4 Asociaciones entre Modelos con Claves Primarias Compuestas

Rails a menudo puede inferir la información de clave primaria - clave externa entre modelos asociados con claves primarias compuestas sin necesitar información adicional. Toma el siguiente ejemplo:

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

class Book < ApplicationRecord
  belongs_to :order
end

Aquí, Rails asume que la columna :id debe usarse como la clave primaria para la asociación entre una orden y sus libros, al igual que con una asociación regular has_many / belongs_to. Inferirá que la columna de clave externa en la tabla books es :order_id. Acceder a la orden de un libro:

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

book.reload.order

generará la siguiente consulta SQL para acceder a la orden:

SELECT * FROM orders WHERE id = 2

Esto solo funciona si la clave primaria compuesta del modelo contiene la columna :id, y la columna es única para todos los registros. Para usar la clave primaria compuesta completa en las asociaciones, establece la opción foreign_key: en la asociación. Esta opción especifica una clave externa compuesta en la asociación, lo que significa que todas las columnas en la clave externa se usarán para consultar el(los) registro(s) asociado(s). Por ejemplo:

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

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

Acceder al autor de un libro:

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

book.reload.author

usará :first_name y :last_name en la consulta SQL:

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

5 Formularios para Modelos con Clave Primaria Compuesta

Los formularios también pueden construirse para modelos con clave primaria compuesta. Consulta la guía de Ayudantes de Formularios para obtener más información sobre la sintaxis del constructor de formularios.

Dado un objeto de modelo @book con una clave compuesta [:author_id, :id]:

@book = Book.find([2, 25])
# => #<Book id: 25, title: "Some book", author_id: 2>

El siguiente formulario:

<%= form_with model: @book do |form| %>
  <%= form.text_field :title %>
  <%= form.submit %>
<% end %>

Genera:

<form action="/books/2_25" method="post" accept-charset="UTF-8" >
  <input name="authenticity_token" type="hidden" value="..." />
  <input type="text" name="book[title]" id="book_title" value="My book" />
  <input type="submit" name="commit" value="Update Book" data-disable-with="Update Book">
</form>

Nota que la URL generada contiene el author_id y id delimitados por un guion bajo. Una vez enviado, el controlador puede extraer los valores de clave primaria de los parámetros y actualizar el registro. Consulta la siguiente sección para más detalles.

6 Parámetros de Clave Compuesta

Los parámetros de clave compuesta contienen múltiples valores en un solo parámetro. Por esta razón, necesitamos poder extraer cada valor y pasarlos a Active Record. Podemos aprovechar el método extract_value para este caso de uso.

Dado el siguiente controlador:

class BooksController < ApplicationController
  def show
    # Extrae el valor de ID compuesto de los parámetros de URL.
    id = params.extract_value(:id)
    # Encuentra el libro usando el ID compuesto.
    @book = Book.find(id)
    # usa el comportamiento de renderizado predeterminado para renderizar la vista show.
  end
end

Y la siguiente ruta:

get '/books/:id', to: 'books#show'

Cuando un usuario abre la URL /books/4_2, el controlador extraerá el valor de clave compuesta ["4", "2"] y lo pasará a Book.find para renderizar el registro correcto en la vista. El método extract_value puede usarse para extraer arrays de cualquier parámetro delimitado.

7 Fixtures de Clave Primaria Compuesta

Los fixtures para tablas de clave primaria compuesta son bastante similares a las tablas normales. Al usar una columna id, la columna puede omitirse como de costumbre:

class Book < ApplicationRecord
  self.primary_key = [:author_id, :id]
  belongs_to :author
end
# books.yml
alices_adventure_in_wonderland:
  author_id: <%= ActiveRecord::FixtureSet.identify(:lewis_carroll) %>
  title: "Alice's Adventures in Wonderland"

Sin embargo, para soportar relaciones de clave primaria compuesta, debes usar el método composite_identify:

class BookOrder < ApplicationRecord
  self.primary_key = [:shop_id, :id]
  belongs_to :order, foreign_key: [:shop_id, :order_id]
  belongs_to :book, foreign_key: [:author_id, :book_id]
end
# book_orders.yml
alices_adventure_in_wonderland_in_books:
  author: lewis_carroll
  book_id: <%= ActiveRecord::FixtureSet.composite_identify(
              :alices_adventure_in_wonderland, Book.primary_key)[:id] %>
  shop: book_store
  order_id: <%= ActiveRecord::FixtureSet.composite_identify(
              :books, Order.primary_key)[:id] %>

Comentarios

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

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

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