06: ACTIVE RECORD: Migraciones de Active Record - LPC-Ltda/Ruby-on-Rails GitHub Wiki
Paso de bebé a las cuatro en punto. Paso de bebé a las cuatro en punto. -Bob Wiley
Es un hecho de la vida que los esquemas de las bases de datos de sus aplicaciones pueden evolucionar en el curso del desarrollo. Tablas son agregadas, nombres de columnas cambiados, cosas son eliminadas -usted tiene la imagen. Sin estrictas convenciones y disciplina de proceso para que los desarrolladores de aplicaciones las sigan, mantener el esquema de base de datos al mismo ritmo que se codifica la aplicación es tradicionalmente un trabajo muy problematico.
Las migraciones son la forma de Rails de ayudarlo a evolucionar el esquema de base de datos de su aplicación (también conocido como DDL) sin tener que caer en re-crear la base de datos cada vez que hace un cambio. Lo cual significa que usted no pierde su data de desarrollo. Esto puede o no ser muy importante pero es usualmente muy conveniente. Los únicos cambios hechos cuando usted ejecuta una migración son aquellos necesarios para mover el esquema desde una versión a otra, ya sea hacia adelante o hacia atrás en el tiempo.
Por supuesto, estar habilitado para evolucionar su esquema sin tener que re-crear su base de datos y la carga/re-carga de data es un orden de magnitud más importante una vez que usted está en producción.
Rails provee un generador para crear migraciones.
$ rails generate migration
Usage:
rails generate migration NAME [field[:type][:index] field[:type][:index]] [options]
Como mínimo usted debe proporcionar un nombre descriptivo para la migración en CamelCase (o texto con undercore ya que ambos funcionan), y el generador hace el resto. Otros generadores, como los de modelos o scaffolding, también crean scripts de migraciones para usted, a menos que usted especifique la opción --skip-migration
.
La parte descriptiva del nombre de la migración depende de usted, pero la mayoría de los desarrolladores Rails que conozco tratan que coincida con la operación del esquema (en los casos simples) o al menos alude lo que ocurre dentro (en los casos más complejos).
Note que si usted cambia el nombre de la clase de su migración a algo que no coincida con su nombre de archivo, usted obtendrá un error
uninitialized constant
cuando la migración es ejecutada.
El workflow completo comienza con la generación de una nueva migración, editando su fuente (si es necesario), y luego corriendo rails db:migrate
desde su terminal.
Si el nombre de la migración es de la forma "CreateXXX" y es seguido por una lista de nombres de columnas y tipos, entonces una migración que crea la tabla XXX con las columnas listadas listadas serán generadas. Por ejemplo:
$ rails g migration CreateProducts name:string part_number:string
genera
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.string :part_number
end
end
end
Si el nombe de la migración es de la forma "AddXXXToYYY" or "RemoveXXXFromYYY" y es seguido por una lista de nombres de columnas y tipos entonces la migración la migración contiene que contienen las instrucciones add_column
y remove_column
serán creadas.
$ rails g migration AddPartNumberToProducts part_number:string
generará
class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
end
end
Si usted desea agregar un índice a la nueva columna, usted puede hacerlo también:
$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
generará
class AddPartNumberToProduct < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
add_index :products, :part_number
end
end
Agregue tantas columnas como usted necesite.
$ rails g migration AddDetailToProducts part_number:string price:decimal
genera
class AddDetailToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :part_number, :string
add_column :products, :price, :decimal
end
end
El generador de migraciones producirá tablas join si "JoinTable" es parte del nombre.
$ rails g migration CreateJoinTableCustomerProduct customer product
lo cual produce la siguiente migración:
class CreateJoinTableCustomerProduct < ActiveRecord::Migration[5.0]
def change
create_join_table :customers, :products do |t|
# t.index [:customer_id, :product_id]
# t.index [:product_id, :customer_id]
end
end
end
Originalmente, las migraciones eran secuenciadas vía un esquema simple numerado respaldado dentro del nombre del archivo migración y automáticamente manejada por el generador de migraciones. Cada migración recibía un número secuencial. Hubo muchos inconvenientes inherentes a ese enfoque, especialmente en ambientes de equipo donde dos desarrolladores pueden chequear una migración con el mismo número de secuencia. Afortunadamente estos temas han sido eliminados por el uso de timestamps para secuenciar migraciones.
Las migraciones que ya han sido ejecutadas son listadas en una tabla de base de datos especial que Rails mantiene. Su nombre es schema_migrations
y sólo tiene una columna:
mysql> desc schema_migrations;
+----------+----------------+---------+---------+----------+--------+
| Field | Type | Null | Key | Default | Extra |
+----------+----------------+---------+---------+----------+--------+
| version | varchar(255) | NO | PRI | NULL | |
+----------+----------------+---------+---------+----------+--------+
1 row in set (0.00 sec)
La primera cosa que rails db:migrate
hará será chequear la tabla schema_migrations
y ejecutar las migraciones en su sistema de archivos que aún no se han ejecutado (incluso si tienen timestamps anteriores a las migraciones que usted ha agregado en el intertanto).
Rails nos empuja hacia la definición de migraciones reversibles. En las versiones antiguas, cada clase de migración tenía dos métodos de instancia llamados up
y down
. El método up
incluye la lógica de qué cambiar en la base de datos para la migración, mientras que el método down
incluye la lógica de cómo revertir/roll-back ese cambio.
A pesar de que hoy día es aún posible usar up
y down
, hoy en día usamos el método change
. Para la mayoría de las operaciones, Rails es suficientemente inteligente como para imaginar cómo hacer el roll back automáticamente.
El siguiente archivo de migración 20130313005347_create_clients.rb ilustra la creación de una nueva tabla llamada clients
:
class CreateClients < ActiveRecord::Migration
def change
create_table :clients do |t|
t.string :name
t.string :code
t.timestamps
end
end
end
Si vamos a la línea de comandos en nuestro directorio de proyecto y tipeamos rails db:migrate
, la tabla clients
será creada. Rails nos da una salida informativa durante el proceso de migración para que podamos ver que está pasando:
$ rails db:migrate
== CreateClients: migrating ================================
-- create_table(:clients)
-> 0.0448s
== CreateClients: migrated (0.0450s) ========================
¿Se pregunta qué está haciendo [5.0] al final de la clase base de la migración? Resulta que hay pequeñas diferencias en el comportamiento de la API de migración entre Rails 5 y versiones anteriores, por ejemplo, la forma en que automáticamente agrega NOT NULL
a las timestamps.
Los cambios significan que si tuviera que actualizar una aplicación Rails antigua a la versión 5 y volver a ejecutar las migraciones antiguas, obtendría un esquema diferente al que esperaba. El problema se evita mediante la introducción de una capa de compatibilidad, que es donde se incluye el etiquetado de la versión con los corchetes. Si [5.0] no está presente en una clase de migración, se ejecutará con un comportamiento heredado. Sin embargo, Rails mostrará una advertencia pidiéndole que agregue la etiqueta de versión apropiada a las migraciones anteriores.
Es simple hacer el roll back de los cambios si usted cometió un error de desarrollo.
$ rails db:rollback
== 2016112317051o CreateEvents: reverting ====================================================
-- drop_table(:events)
-> 0.0009s
== 2016112317051o CreateEvents: reverted (0.0111s) ===========================================
Si usted alguna vez necesita hacer el roll back de una versión anterior del schema, sólo pase el número de versión que se desea roll back, como en rails db:migrate VERSION=20161123170510
.
Es realmente muy común olvidar agregar algo a la migración. Rails le da rails db:migrate:redo
como una forma conveniente de hacer roll back y volver a migrar todo en un sólo comando.
Si una migración es muy compleja, Active Record puede no estar habilitado a revertir sin un poco de ayuda. El método reversible
actúa en forma muy similar a los métodos de migración up
y down
que eran comunes en las versiones previas de Rails. Al usar reversible
, uno puede especificar operaciones para realizar cuando se ejecuta una migración y otras cuando se revierte.
En el ejemplo siguiente, el método reversible
pasa lógica en un block para los métodos up
y down
para habilitar y deshabilitar soporte hstore en una base de datos Postgres.
def change
reversible do |dir|
dir.up do
execute <<-END
CREATE FUNCTION add(integer, integer) RETURN integer
AS 'select $1 + $2;'
LANGUAGE SQL
INMUTABLE
RETURNS NULL ON NULL INPUT;
END
end
dir.down do
execute "DROP FUNCTION add"
end
end
end
La API de Migraciones no conoce nada acerca de funciones personalizadas, lo cual es por que en el ejemplo tenemos que dejar de hablar de la conexión a la base de datos directamente, usando el método
execute
.
Algunas migraciones son destructivas en una forma que no puede ser reversada. Las migraciones como esta deben levantar una excepción ActiveRecord::IrreversibleMigration
en su bloque reversible down
.
Por ejemplo, que pasa si alguno de su equipo comete un error tonto y define la columna teléfono de su tabla clients como un entero? Usted puede cambiar la columna a un string y la data será migrada claramente, pero ir de string a entero? no se puede.
def change
reversible do |dir|
dir.up do
# Phone number fields are not integers, duh!
change_column :clients, :phone, :string
end
dir.down { raise ActiveRecord::IrreversibleMigration }
end
end
El método create_table
necesita al menos un nombre para la tabla y un block que contenga la definición de columnas. Por que especificamos con símbolos en lugar de strings? Ambos trabajan pero los símbolos requieren menos trabajo.
El método create_table
asume algo grande pero usualmente verdadero, que deseamos una llave numérica (entero) auto-incrementable. Esta es la razón por la cual usted no la ve declarada en la lista de columnas. Si este supuesto esta equivocado es momento de pasarle algunas opciones a create_table
en un hash.
Por ejemplo, como define usted una simple tabla join consistente de dos columnas llaves foráneas y que no necesita su llave primaria? Sólo pase al método create_table
una opción :id
seteada a false
-booleano, no símbolo! Esto evitará que la migración auto-genere ya una clave primaria:
create_table :ingredients_recipes, id: false do |t|
t.column :ingredient_id, :integer
t.column :recipe_id, :integer
end
Alternativamente, la misma funcionalidad puede ser lograda usando el método create_join_table
, cubierto después en este capítulo.
Si todo lo que usted desea es cambiar el nombre de la columna llave primaria de su valor por defecto "id", pase la opción :id
un símbolo a cambio. Por ejemplo, digamos que su corporación manda que las llaves primarias sigan el patrón tablename_id
, entonces el ejemplo luce así:
create_table :clients, id: :clients_id do |t|
t.column :name, :string
t.column :code, :string
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
6.1.8.1 Opciones
La opción force: true
le cuenta a la migración que se realice y que elimine la tabla definida si es que existe. Tenga cuidado con esta, ya que producirá la (posiblemente indeseada) pérdida de data cuando corre en producción. Según yo conozco, la opción :force
es mayormente útil para asegurarse que la migración ponga la base de datos en un estado conocido, pero esto no es una práctica diaria.
La opción :options
le permite agregar instrucciones personalizadas a la instruccion SQL CREATE y es útil para agregar comandos específicos de la base de datos para su migración. Dependiendo de la base de datos que usted está usando, usted estará habilitado para especificar cosas tales como set de caracteres, collation, comentarios, min/max de tamaños, y muchas otras propiedades usando esta opción.
La opción temporary: true
especifica la creación de una tabla temporal que sólo existirá durante la conexión actual a la base de datos. En otras palabras, sólo existe durante la migración. En escenarios avanzados, esta opción puede ser útil para la migración de grandes conjuntos de data desde una tabla a otra, pero no es comúnmente usada.
Un hecho poco conocido es que usted puede remover antiguos archivos de migración (mientras mantiene los nuevos) para mantener el directorio
db/migrate
de un tamaño manejable. Usted puede mover las migraciones antiguas al directoriodb/archived_migrations
o algo como eso. Una vez que usted haya recortado el tamaño de su directorio de migraciones, use la tarearake db:reset
para re-crear su base de datos desdedb/schema.rb
y cargar las seeds dentro de su ambiente actual.
Básicamente trabaja como create_table
y acepta el mismo tipo de definición de columnas.
En Rails 4, un nuevo método de migración create_join_table
ha sido agregado para crear fácilmente tablas join del estilo has_and_belong_to_many
. El método create_join_table
acepta como mínimo el nombre de dos tablas.
create_join_table :ingredients, :recipes
El código precedente creará una tabla llamada "ingredients_recipes" sin llave primaria.
6.1.10.1 Opciones
:table_name Si usted no está de acuerdo con la convención de Rails de concatenar ambos nombres de archivos con un underscore, la opción :table_name
le permite sobrescribirlo explícitamente.
:column_options Agrega algunas opciones extra para agregar a la definición de la columna de llave primaria.
:options, :temporary, y :force Acepta la misma interface que las opciones equivalentes encontradas en create_table
.
class CreateJoinTableUserAuction < ActiveRecord::Migration[5.0]
def change
create_join_table(:users, :auctions, column_options: {type: :uuid})
end
end
Esta sección detalla los métodos que están disponibles en el contexto de los métodos create_table
y change_table
dentro de una clase migración.
6.1.11.1 change(column_name, type, options = {})
Cambia la definición de la columna de acuerdo a las nuevas opciones. El hash de opciones opcionalmente contiene un hash con argumentos que corresponden a las opciones usadas cuando se agregan columnas.
t.change(:name, :string, limit: 80)
t.change(:description, :text)
6.1.11.2 change_default(column_name, default)
Setea un nuevo valor por defecto para la columna.
t.change_default(:qualification, 'new')
t.change_default(:authorized, 1)
6.1.11.3 column(column_name, type, options = {})
Agrega una nueva columna a una tabla nombrada.
t.column(:name, :string)
Note que usted también puede usar la versión abreviada al llamarlo por tipo. Esto agrega una columna (o columnas) de un tipo especificado (string, text, integer, float, decimal, datetime, time, date, binary, o booleano).
t.string(:goat)
t.string(:goat, :sheep)
t.string(:age, :quantity)
Los tipos de columna básicos soportados por la mayoría de los adaptadores de bases de datos están listados en la tabla 6.1.
Tabla 6.1 Tipos de columnas más comúnmente usados en Rails
Tipo Migración | MySQL | Postgres | SQLite | Oracle | Clase Ruby |
---|---|---|---|---|---|
:binary | blob | bytea | blob | blob | String |
:boolean | tinyint(1) | boolean | boolean | number(1) | Boolean |
:date | date | date | date | date | Date |
:datetime | datetime | timestamp | datetime | date | Time |
:decimal | decimal | decimal | decimal | decimal | BigDecimal |
:float | float | float | float | number | Float |
:integer | int(11) | integer | integer | number(38) | Fixnum |
:string | varchar(255) | character varying(255) | varchar(255) | varchar2(255) | String |
:text | text | text | text | clob | String |
:time | time | time | time | date | Time |
:timestamp | datetime | timestamp | datetime | date | Time |
Aprenda más acerca de definir columnas más tarde en este capitulo.
6.1.11.4 index(column_name, options = {})
Agrega un nuevo índice a la tabla. El parámetro column_name
puede ser un símbolo o un arreglo de indices que refieren las columnas que serán indexadas. El parámetro name
le permiten sobre-escribir el nombre por defecto que sera de otra forma ser generado.
# a simple index
t.index(:name)
# a unique index
t.index([:branch_id, :party_id], unique: true)
# a named index
t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
Rails tiene soporte bulid-in para indices parciales de PostgreSQL (ver https://www.postgresql.org/docs/7.0/static/partial-index.htm). Usted puede especificarlo en su migtración agregando una opción :where
a la declaración de índice normal. El beneficio principal es la reducción del tamaño de los índices sobre las consultas comunmente usadas dentro de una aplicación.
Por ejemplo, asumamos que una aplicación Rails consulta constantemente por los clientes que tienen un estado "active" dentro del sistema. En lugar de crear un índice sobre la columna status para cada registro cliente, podemos incluir sólo aquellos registros que cumplen un criterio especificado.
add_index(:clients, :status, where: 'active')
Rails también tiene soporte built-in para indices de expresion PostgreSQL, con clases de operadores opcionales (ver https://www.postgresql.org/docs/9.4/static/indexes-opclass.html). Tome nota de la expresión y operador SQL en la línea 3.
def change
add_index :users, 'lower(last_name) varchar_pattern_ops',
name: "index_users_on_name_unique",
unique: true
end
En lugar de un nombre de columna, hay una expresión SQL especificando que el valor del índice debe ser una representación en minúsculas del apellido del usuario.
La clase de operador varchar_pattern_ops
(también en la línea 3) es especialmente útil para campos en los que sabe que va a hacer LIKE o consultas regexp . Esto cambia la forma en que el índice analiza la columna en la cual se basa la recopilación específica local a un B-tree de carácter por carácter. Solo tenga cuidado de crear también índices normales si es necesario, o algunas de sus consultas podrían terminar haciendo escaneos de tabla completa y ejecutarse muy lentamente.
6.1.11.5 belongs_to(args)
y references(args)
Estos dos métodos son alias el uno del otro. Ellos agregan una columna llave externa a otro modelo, usando la convención de nombres de Active Record. Opcionalmente agrega una columna _type
si la opción :polymorphic
esta seteada en true.
create_table :accounts do
t.belongs_to(:person)
end
create_table :comments do
t.references(:commentable, polymorphic: true)
end
Una buena práctica común es crear un índice para cada llave externa en sus tablas de bases de datos. Es tan común que Rails 4 introdujo una opción :index
a los métodos references
y belongs_to
que crea un índice para las columnas inmediatamente después de la creación. La opción index
acepta un valor booleano o el mismo hash de opciones que el método index
, cubierto en la sección anterior.
create_table :accounts do
t.belong_to(:person, index: false)
end
6.1.11.6 remove(*column_names)
Remueve la columna(s) especificadas de la definición de tablas.
t.remove(:qualification)
t:remove(:qualification, :experience)
6.1.11.7 remove_index(options = {})
Remueve un índice dado de una tabla.
# Remove the accounts_branch_id_index from the account table.
t.remove_index column: :branch_id
# Remove the accounts_branch_id_party_id_index from the accounts table.
t.remove_index column: [:branch_id, :party_id]
# Remove the index named by_branch_party in the accounts table.
t.remove_index name: :by_branch_party
6.1.11.8 remove_references(*args)
y remove_belongs_to
Remueve una referencia. Opcionalmente remueve una columna type
.
t.remove_belongs_to(:person)
t.remove_references(:commentable, polimorphic: true)
6.1.11.9 remove_timestamps
Usted nunca usará este método. Este remueve las columnas created_at
y updated_at
.
6.1.11.10 rename(old_column_name, new_column_name)
Renombra una columna. El viejo nombre va primero -un hecho que usualmente no recuerdo.
t.rename :description,
6.1.11.11 revert
Si alguna vez usted buscó revertir un archivo de migración específico dentro de otra migración, ahora usted puede. El método revert
puede aceptar el nombre de una clase migración, lo cual cuando es ejecutado, revierte la migración dada.
revert CreateProductsMigration
El método revert
puede también aceptar un bloque de directivas para reversar en ejecución.
6.1.11.12 timestamps
Agrega las columnas timestamps mantenidas por Active Record (created_at
y updated_at
) a la tabla.
t.timestamps
Desde Rails 5, los timestamps son marcados automaticamente como NOT NULL
.
Las columnas pueden ser agregadas a una tabla usando el método column
, dentro del bloque de una instrucción create_table
, o con el método add_column
. Aparte de tomar el nombre de la tabla para agregar la columna como su primer argumento, los métodos trabajan idénticamente.
create_table :clients do |t|
t.column :name, :string
end
add_column :clients, :code, :string
add_column :clients, :created_at, :datetime
El primer (o segundo) parámetro obviamente especifica el nombre de la columna, y el segundo (o tercero) especifica su tipo. El estándar SQL92 define los tipos de datos fundamentales, pero cada base de datos implementa su propia variación del estándar.
Rails tiene sus propios nombres generalizados para tipos de columnas, los cuales fueron resumidos antes en este capítulo en la tabla 6.1. Si usted está familiarizado con los tipos de columnas de bases de datos, cuando examinó esa tabla le podría haber parecido un poco extraño que haya una columna de base de datos declarada como tipo :string
, ya que la base de datos no tiene columnas string, tienen tipos char o varchar.
La razón para declarar una columna de base de datos del tipo string es que las migraciones de Rails tienen la intención de ser agnósticos de bases de datos. Esto es porque usted puede (como yo lo he hecho en ocasiones) desarrollar usando Postgres como su base de datos y desplegar en producción en Oracle.
Una completa discusión acerca de cómo elegir el tipo de datos correcto que su aplicación necesita está fuera del alcance de este libro. Sin embargo, es útil tener una referencia de cómo los tipos genéricos de la migración mapean a los tipos específicos de la base de datos. El mapeo para las bases de datos más comúnmente usadas con Rails está en la Tabla 6.2.
Tabla 6.2 Columnas mapeadas para las bases de datos más comunmente usadas con Rails
6.2.1.1 Tipos nativos de columnas de bases de datos
Cada clase adaptador de conexión tiene un hash native_database_types
, el cual establece el mapeo descrito en la tabla 6.2. Si usted necesita mirar el mapeo para una base de datos no listada en la Tabla 6.1, usted puede abrir el código de adaptador de Ruby y buscar el hash native_database_types
, como el siguiente dentro de la clase PostgreSQLAdapter
dentro del archivo postgresql_adapter.rb
:
NATIVE_DATABASE_TYPES = {
primary_key: "serial primary key",
string: { name: "character varying", limit: 255 },
text: { name: "text" },
integer: { name: "integer" },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
daterange: { name: "daterange" },
numrange: { name: "numrange" },
tsrange: { name: "tsrange" },
tstzrange: { name: "tstzrange" },
int4range: { name: "int4range" },
int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
xml: { name: "xml" },
tsvector: { name: "tsvector" },
hstore: { name: "hstore" },
inet: { name: "inet" },
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
json: { name: "json" },
jsonb: { name: "jsonb" },
ltree: { name: "ltree" },
citext: { name: "citext" },
point: { name: "point" },
line: { name: "line" },
lseg: { name: "lseg" },
box: { name: "box" },
path: { name: "path" },
polygon: { name: "polygon" },
circle: { name: "circle" },
bit: { name: "bit" },
bit_varying: { name: "bit_varying" },
money: { name: "money" },
}
Usted habrá notado que el adaptador PostgreSQL incluye una gran cantidad de mapeos de tipos de columnas que no están disponibles en otras bases de datos. Usted puede especificar estos tipos de columnas en su migración y trabajarán muy bien, pero perderá portabilidad de base de datos.
La forma más fácil de echar un vistazo al código del adaptador es en Github.
Profundizamos en por qué es posible que desee utilizar tipos de columnas extendidas, como hstore
y array
en el Capítulo 9 "Active Record avanzado".
Para muchos tipos de columnas, sólo especificar el tipo no es información suficiente. Todas las declaraciones de columnas aceptan las siguientes opciones:
default: value
Esto setea un valor por defecto para ser usado como el valor inicial de la columna para una nueva fila. Usted nunca necesitará setear explícitamente el valor por defecto a null
. Sólo no utilice esta opción para tener un valor por defecto null
. Es bueno notar que MySQL 5.x ignora los valores por defecto para columnas binary y text.
limit: size
Esto agrega un parámetro tamaño para columnas string, text, binary o integer. Esto significa distinto dependiendo del tipo de columna a la que se aplique. Generalizando, limit para el tipo string se refiere al número de caracteres, mientras que para los otros tipos, se refiere al número de bytes usados para almacenar el valor en la base de datos.
null: false
Esto hace que la columna sea requerida e nivel de la base de datos agregando una restricción not null
.
index: true
Agrega un índice común generado para la columna.
comment: text
Agrega un comentario para la columna que será visible en schema.rb
y en cierto tipo de software administrador de bases de datos.
La opción
comment
es nueva en Rails 5 y es especialmente útil en equipos grandes donde no siempre es posible mantenerse al día exactamente con lo que hace cada nueva columna agregada a la base de datos. Actualmente sólo MySQL y PostgreSQL proveen comentarios.
6.2.2.1 Precisión decimal
Las columnas declaradas como :decimal
aceptan las siguientes opciones:
precision: number
Precisión es en número total de dígitos de un número.
scale: number
Scale es el número de dígitos a la derecha del punto decimal. Por ejemplo el número 123.45 tiene una precision
de 5 y una scale
de 2. Lógicamente la escala no puede sera mayor que la precisión.
Los tipos decimales tienen una seria oportunidad de pérdida de data durante las migraciones de data de producción entre diferentes tipos de bases de datos. Por jemplo, la precisión por defecto entre Oracle y SQL Server puede causar que el proceso de migración trunque y cambie el valor de su data numérica. Es siempre una buena idea especificar detalles de precisión para su data.
La elección del tipo de columna no es simple y depende tanto de la base de datos como de los requerimientos de su aplicación.
:binay
dependiendo de su particular escenario de uso, almacenar data binaria en la base de datos puede causar enormes problemas de desempeño. Active Record no excluye generalmente ninguna columna cuando carga objetos desde una base de datos, y poner enormes atributos binarios en modelos comúnmente usados incrementará la carga sobre su servidor de bases de datos significativamente. Si usted debe poner contenido binario en una clase comúnmente usada, tome ventaja del método :select
para sólo traer las columnas que usted necesita.
:boolean
La forma en que los valores booleanos son almacenados varía de base de datos en base de datos. Algunas usan los valores enteros 1 y 0 para representar verdadero y falso, respectivamente. Otras usan caracteres tales como T y F. Rails maneja en mapeo entre el true
y false
de Ruby muy bien, así que usted no necesita preocuparse acerca del esquema subyacente por usted mismo. Setear atributos directamente a los valores de la base de datos como 1 o F puede funcionar pero es considerado un anti-pattern.
:datetime
y :timestamp
La clase Ruby que Rails mapea a las columnas datetime
y timestamp
es Time
. En un ambiente de 32_bits, Time
no funciona para fechas anteriores a 1902. La clase Ruby DateTime
funciona con fechas anteriores a 1902, y Rails retrocede a usarlo si es necesario. No usa DateTime
por razones de desempeño. Bajo la cubierta, Time
es implementado en C y es muy rápido, mientras que DateTime
está escrito en Ruby puro y es comparativamente más lento.
:time
Es muy raro que usted desee usar un tipo de data :time
-quiza si usted está modelando un reloj alarma. Rails leerá el contenido de la base de datos como valores hora, minuto y segundo dentro del objeto Time
con valores dummy para el año, mes y día.
:decimal Las antiguas versiones de Rails (anteriores a la 1.2) no soportaban el tipo :decimal
de precisión fija, y como resultado muchas aplicaciones incorrectamente usaban tipos de datos :float
. Los tipos de datos de punto flotante son por naturaleza imprecisos, por lo que es importante elegir :decimal
en lugar de :float
para las aplicaciones relacionadas con negocios.
Si usted está usando un float para almacenar valores que necesitan ser precisos, como dinero, usted es un burro (jackass). Los cálculos de punto flotante son hechos en binario más bien que en decimal, errores de redondeo abundan en lugares donde usted no lo espera.
>> 0.1+0.2 == 0.3
=> false
>> BigDecimal('0.1') + BigDecimal('0.2') == BigDecimal('0.3')
=> true
:float
No use float para almacenar valores de dinero, o más precisamente, cualquier tipo de data que necesita precisión fija. Ya que los números de punto flotante son más bien aproximaciones, una simple representación de un número como un float esta probablemente OK. Sin embargo, una vez que usted comienza haciendo operaciones matemáticas o comparación de valores float, es ridículamente fácil introducir bugs difíciles de diagnosticar en su aplicación.
:integer y :string No hay muchas trampas que se me ocurran cuando se trata de números enteros y strings. Ellos son los bloques de construcción básicos de su aplicación, y muchos desarrolladores de Rails dejan de especificar el tamaño, lo cual resulta en un máximo por defecto de 11 digitos y 255 caracteres, respectivamente. Usted debe tener en mente que no obtendrá un error si trata de almacenar valores que exceden el máximo tamaño definido para la columna de la base de datos, su string será simplemente truncado. Use validaciones para estar seguro que la data ingresada por el usuario no excede el máximo tamaño permitido.
:text Hay reportes de campos text que disminuyen el desempeño de las consultas en algunas bases de datos -suficiente para tener en consideración en aplicaciones que necesitan escalar a altas cargas. Si usted debe usar una columna text en una aplicación de performance crítica, póngala en una tabla separada.
Preservando tipos de datos personalizados
Si el uso de tipos de datos específicos (como
:double
, para mayor precisión que:float
) es crítico para su proyecto, use el seteoconfig/active_record.schema_format = :sql
enconfig/application.rb
para hacer un vaciado Rails de la información del schema en el formato SQL DDL más que su propio código Ruby compatible cross-plataform, vía el archivodb/schema.rb
.
Rails hace magia con las columnas datetime si ellas están nombradas de una cierta forma. Active Record automáticamente hará el timestamp de la operación create si la tabla tiene una columna llamada created_at
o created_on
. Lo mismo aplica a las updates cuando hay columnas llamadas updated_at
o updated_on
.
Note que created_at
y updated_at
deben ser definidas como datetime
pero si usted usa t.timestamps
, entonces no tendrá que preocuparse del tipo de columnas que son.
El timestampeo automático puede ser apagado globalmente setando la siguiente variable en un inicializador.
ActiveRecord::Base.record_timestamps = false
El código anterior apaga los timestamps para todos los modelos, pero record_timestamps
es heredable, así que usted puede hacerlo caso a caso al setar self.record_timestamps
a false
en la cima de un modelo específico.
Una cantidad de modificadores de tipos de columnas usados comúnmente puede ser pasado directamente sobre la línea de comandos. Ellos son encerrados en llaves a continuación del tipo del campo.
Por ejemplo ejecute:
$ rails g migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
producirá una migración que luce como esto:
class AddDetailsToProducts < ActiveRecord::Migration[5.0]
def change
add_column :products, :price, :decimal, precision: 5, scale: 2
add_reference :products, :supplier, polymorphic: true
end
end
Esta magia particular no es una área bien documentada de Rails. Esta es "magia negra" si desea y se divierte experimentando con ella.
Rails normalmente intenta ejecutar sus migraciones dentro de una transacción, si esa funcionalidad es soportada por su base de datos. (La mayoría lo hace) Ocasionalmente, esto puede causar in tema si usted intenta hacer algo que no funciona dentro de una transacción (como agregar cierto tipo de índices).
Si usted ejecuta este tipo de temas, usted puede apagar las transacciones para una migración particular usando el método de clase disable_ddl_transaction!
.
class AddConcurrentIndexToBids < ActiveRecord::Migration[5.0]
disable_ddl_transaction!
def change
reversible do |dir|
dir.up do
execute "CREATE INDEX CONCURRENTLY index_auction_id ON bids(auction_id)"
end
dir.down do
execute "DROP INDEX index_auction_id"
end
Hasta el momento sólo hemos discutido el uso de archivos de migración para modificar el esquema de su base de datos. Inevitablemente, iremos a situaciones donde usted también necesita realizar migración de datos, en conjunción con un cambio de esquema o no.
En la mayoría de los casos, usted debe diseñar la migración de su data en SQL puro usando el comando execute
que está disponible dentro de la clase migración.
Por ejemplo, digamos que usted tiene una tabla phones
que pone números telefónicos en sus partes componentes, y usted desea más tarde simplificar su modelo teniendo sólo una columna number
a cambio. Usted escribirá una migración como esta:
class CombineNumberInPhones < ActiveRecord::Migration
def change
add_column :phones, :number, :string
reversible do |dir|
dir.up{execute("update phones set number=concat(area_code, prefix, suffix)")}
dir.down {...}
end
remove_column :phones, :area_code
remove_column :phones, :prefix
remove_column :phones, :suffix
end
end
La alternativa naive a usar SQL en el ejemplo previo tendrá más líneas de código y será mucho más lenta.
Phone.find_each do |p|
p.number = p.area_code + p.prefix + p.suffix
p.save
end
En este caso particular, usted puede usar el método de Active Record update_all
para mantener la migración en una sóla línea.
Phone.update_all("set number = concat(area_code, prefix, suffix)")
Aunque admito que es tentador hacerlo en lugar de piratear algunos SQL con los que quizás no te sientas cómodo, el peligro viene por el camino mientras tu esquema y diseño de modelo continúan envolviendo. Aunque es poco probable en este ejemplo, es posible que el teléfono no tenga la misma configuración y comportamiento en el futuro como lo hace hoy.
En la siguiente sección, ahondaremos en como usted se protege a si mismo de esta situación escribiendo un modelo stand alone simple de Phone
en el script de migración mismo.
Le puedo decir desde la experiencia real que introducir modelos de migración específicos puede traer desorden y de dificil debug. Yo recomiendo apegarse al SQL puro en migraciones donde sea posible.
Si usted declara un modelo de migración dentro del script de migración, este será "namespaced" a la clase de migración.
class HashPasswordsOnUsers < ActiveRecord::Migration
class User < ActiveRecord::Base
end
def change
reversible do |dir|
dir.up do
add_column :users, :hashed_password, :string
User.reset_column_information
User.find_each do |user|
user.hashed_password = Digest::SHA1.hexdigest(user.password)
user.save!
end
remove_column :users, password
end
dir.down { raise ActiveRecord::IrreversibleMigration }
end
end
end
Por que no usa sólo sus clases de modelo de su aplicación en el script de migración directamente? En la medida que su esquema evoluciona, las antiguas migraciones que usan las clases de modelo directamente se podrán romper y hacerse no usables. Los apropiados modelos de migración con namespacing previenen que usted tenga que lamentar enfrentamiento de nombres con sus clases de modelo de la aplicación o con aquellos que son definidos en otras migraciones.
Note que Active Record pone en cache la información de las columnas en el primer requerimiento a la base de datos, así que si usted desea realizar una migración de data inmediatamente después de la migración, usted caerá en una situación donde las nuevas columnas aún no han sido cargadas. Este es el caso donde usando
reset_column_information
puede ser útil. Simplemente llame este método de clase sobre su modelo y todo será vuelto a cargar en el siguiente requerimiento.
##6.3 schema.rb
El archivo schema.rb
es generado cada vez que usted migra y refleja el último estado de su esquema de base de datos. Usted no debe nunca editar el archivo db/schema.rb
manualmente ya que este archivo es auto.generado desde el estado actual de la base de datos. En lugar de editar este archivo, por favor use la característica migraciones de Active Record para incrementalmente modificar su base de datos, y entonces regenerar esta definición de schema.
Note que esta definición schema.rb
es la fuente de autoría para su esquema de base de datos. Si usted necesita crear la base de datos de la aplicación en otro sistema, usted debería usar db:schema:load
, no correr todas las migraciones desde cero. El último es un enfoque erróneo e insostenible (mientras más migraciones más lento correrá y mayor probabilidad de errores).
Esta fuertemente recomendado chequear este archivo dentro de su sistema de control de versiones. Primero que nada, ayuda tener una definición de esquema definitiva como referencia. Segundo, usted puede correr rake db:schema:load
para crear su esquema de base de datos desde cero sin tener que correr todas las migraciones. Esto es especialmente importante considerando que, en la medida que su proyecto evoluciona, es probable que se haga imposible correr todas las migraciones desde un comienzo debido a incompatibilidad de código, tal como el re-nombramiento de clases nombradas explícitamente.
##6.4 Alimentación de la base de datos
El archivo creado automáticamente db/seeds.rb
es una ubicación por defecto para crear data para alimentar su base de datos. Fue introducida con el propósito de detener la práctica de insertar data en archivos de migración individuales si usted acepta la premisa de que las migraciones nunca debieran servir para alimentar ejemplos o data básica requerida para la aplicación. Esta es ejecutada con la tarea rake db:seed
(o creada junto con la base de datos cuando ejecuta rake db:setup
).
En su forma más simple, el contenido de seed.rb
es sólo una serie de instrucciones create!
que generan la data base para su aplicación, si es por defecto o relacionada con la configuración. Por ejemplo, agreguemos un usuario administrador y algunos códigos de facturación para nuestra aplicación de tiempo y gasto:
User.create!(login: 'admin', email: '[email protected]', :password: '123',
password_confirmation: '123', authorized_approver: true)
client = Client.create!(name: 'Workbeast', code: 'BEAST')
client.billing_codes.create!(name: 'Meetings', code: 'MTG')
client.billing_codes.create!(name: 'Development', code: 'DEV')
Por qué usar la versión bang del método create? Porque de otra forma, usted no podrá averiguar si tiene errores en su archivo seed. Una alternativa sería usar el método first_or_create
para hacer seeding idempotente.
c = Client.where(name: 'Workbeast', code: 'BEAST').first_or_create!
c.billing_codes.where(name: 'Meetings', code: 'MTG').first_or_create!
c.billing_codes.where(name: 'Development', code: 'DEV').first_or_create!
Otra práctica común de seeding que vale la pena mencionar es llamar a delete_all
antes de llamar a la creación de un nuevo registro, de esta forma el seeding no genera registro duplicados. Esta práctica elimina la necesidad de rutinas de seeding idempotentes y lo deja muy seguro acerca de cómo lucirá la base de datos después de la alimentación.
User.delete_all
User.create!(login: 'admin', ...
Client.delete_all
client = Client.create!(name: 'Workbeast', ...
Yo uso típicamente el archivo
seed.rb
para data que es escencialmente para todos los ambientes incluido producción. Para datos tontos que puede ser sólo usada en desarrollo o en la puesta en escena. Yo prefiero crear tareas rake personalizadas bajo el directoriolib/task
-por jemplo,lib/task/load_dev_data.rake
. Esto ayuda a mantenerseed.rb
limpio y libre de condiciones innecesarias, comounless Rails.env.production?
.
##6.5 Tareas rake relacionadas con la base de datos
Las siguientes tareas rake están incluidas por defecto en los proyectos Rails
6.5.0.1 db:create
y db:create:all
Crea la base de datos definida en config/database.yml
para el Rails.env
actual. Si el ambiente actual es development, Rails creará tanto la base de datos local de development como la de test (o crea todas las bases de datos locales definidas en config/database.yml
en el caso de ejecutar db:create:all
.
6.5.0.2 db:drop
y db:drop:all
Drops (bota) la base de datos del actual RAILS_ENV
. Si el ambiente actual es development, "bota" tanto la base de datos local de development como la test. (o bota todas las bases de datos definidas en config/database.yml
en el caso de ejecutar db:drop:all
.
6.5.0.3 db:forward
y db:rollback
La tarea db:rollback
mueve su esquema de base de datos hacia atrás una versión. Similarmente, la tarea db:forward
mueve su esquema de base de datos hacia adelante una versión y es típicamente usado después de un rollback.
6.5.0.4 db:migrate
Aplica todas las migraciones pendientes. Si una variable de ambiente VERSION es provista, entonces db:migrate
aplicará a las migraciones pendientes hasta la migración especificada pero no posteriores. La VERSION es especificada como la porción timestamp del archivo de migración.
# example of migrating up with param
$ rake db:migrate VERSION=20130313005347
== CreateUsers: migrating ==================================
-- create_table(:users)
-> 0.0014s
== CreateUsers: migrated (0.0015s) ==========================
Si la VERSION provista es más antigua que la versión actual del esquema, entonces esta migración hará un rollback de las versiones más nuevas.
# example of migrating down with param
$ rake db:migrate VERSION=20130312152614
== CreateUsers: reverting ==================================
-- drop_table(:users)
-> 0.0014s
== CreateUsers: migrated (0.0015s) ==========================
6.5.0.5 db:migrate:down
Ejecuta el método down
de una migración especificada solamente. La VERSION es especificada como la porción timestamp del nombre del archivo de migración.
# example of migrating down with param
$ rake db:migrate:down VERSION=20130316172801
== CreateClients: reverting ==================================
-- drop_table(:clients)
-> 0.0028s
== CreateClients: migrated (0.0054s) ==========================
6.5.0.6 db:migrate:up
Ejecuta el método up
de una migración especificada solamente. La VERSION es especificada como la porción timestamp del nombre del archivo de migración.
# example of migrating down with param
$ rake db:migrate:up VERSION=20130316172801
== CreateClients: migrating ==================================
-- create_table(:clients)
-> 0.0260s
== CreateClients: migrated (0.0261s) ==========================
6.5.0.7 db:migrate:redo
Ejecuta el método down
de la último archivo de migración inmediatamente seguido por su método up
. Esta tarea es típicamente usada justo después de corregir un error en el método up
o para probar que la migración está corriendo correctamente.
$ rake db:migrate:redo
== AddTimesheetUpdatedAtTo Users: reverting ====================
-- remove_column(:users, :timesheets_updated_at)
-> 0,0853s
== AddTimesheetUpdatedAtToUsers: reverted (0.0861s) =============
== AddTimesheetUpdatedAtToUsers: migrating ====================
-- add_column(:users, :timesheets_updated_at, :datetime)
-> 0.3577s
== AddTimesheetUpdatedAtToUsers: migrated (0.3579s) =============
6.5.0.8 db:migrate:reset
Resetea su base de datos para el ambiente actual usando sus migraciones (como opuesto a usar schema.rb
).
###6.5.1 db:migrate:status
Despliega el estado de todas las migraciones existentes en una tabla bien formateada. Esta mostrará up
para las migraciones que han sido aplicadas y down
para aquellas que no.
Esta tarea es útil en situaciones donde usted chequear los cambios recientes al esquema antes de realmente aplicarlos (justo después de bajarlos desde un repositorio remoto, por ejemplo).
$ rake db:migrate:status
database: timesheet_development
Status Migration ID Migration Name
----------------------------------------------------------
up 20130219005505 Create users
up 20130219005637 Create timesheets
up 20130220001021 Add user id to timesheets
down 20130220022039 Create events
6.5.1.1 db:reset
y db:setup
El db:setup
crea la base de datos para el ambiente actual, carga el esquema desde db/schema.rb
, y entonces carga la alimentación de data. Esto es usado cuando usted está seteando un proyecto existente por primera vez en una estación de trabajo de desarrollo. La tarea similar db:reset
hace la misma cosa excepto que deja y re-crea la base de datos primero.
6.5.1.2 db:schema:dump
Crea un archivo db/schema.rb
que puede ser portablemente usado para contra cualquier base de datos soportada por Active Record. Note que la creación (o actualización) de schema.rb
pasa automáticamente cada vez que usted realiza una migración.
6.5.1.3 db:schema:load
Carga el archivo schema.rb
dentro de la base de datos para el ambiente actual.
6.5.1.4 db:seed
Carga la data de alimentación desde db/seeds.rb
como se describe en la sección de este capítulo "Alimentación de bases de datos".
6.5.1.5 db:structure:dump
Vacía la estructura de base de datos a un archivo SQL que contiene código DLL (data description language) puro en un formato correspondiente al driver de la base de datos especificado en database.yml
para su ambiente actual.
$ rake db:structure:dump
$ cat db/development_structure.sql
CREATE TABLE 'avatars' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'user_id' int(11) DEFAULT NULL,
'url' varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
PRIMARY KEY ('id')
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
...
Rara vez he necesitado usar esta tarea. Es posible que algunos equipos Rails trabajando en conjunción con DBAs que ejercen control estricto sobre sus esquemas de bases de datos de aplicación necesitarán esta tarea regularmente.
6.5.1.6 db:test:prepare
Chequea migraciones pendientes y carga el esquema test al hacer un db:schema:dump
seguido por un db:schema:load
.
Esta tarea es usada muy frecuentemente durante el desarrollo activo donde quiera que usted esté corriendo specs o pruebas sin usar rake. (La tarea rake stándar relacionada con spec corre db:test:prepare
automáticamente por usted.)
6.5.1.7 db:version
Retorna el timestamp del último archivo migración que ha sido corrido. Trabaja incluso si su base de datos ha sido creada desde db/schema.rb
, ya que este contiene el último timestamp de versión en él:
ActiveRecord::Schema.define(version: 20130316172801)