Usando Rails, ¿cómo puedo configurar mi clave principal para que no sea una columna de tipo entero?

85

Estoy usando migraciones de Rails para administrar un esquema de base de datos y estoy creando una tabla simple en la que me gustaría usar un valor no entero como clave principal (en particular, una cadena). Para abstraerme de mi problema, digamos que hay una tabla employeesdonde los empleados se identifican mediante una cadena alfanumérica, por ejemplo "134SNW".

Intenté crear la tabla en una migración como esta:

create_table :employees, {:primary_key => :emp_id} do |t|
    t.string :emp_id
    t.string :first_name
    t.string :last_name
end

Lo que esto me da es lo que parece que ignoró por completo la línea t.string :emp_idy siguió adelante y la convirtió en una columna entera. ¿Hay alguna otra forma de que los rieles generen la restricción PRIMARY_KEY (estoy usando PostgreSQL) por mí, sin tener que escribir el SQL en una executellamada?

NOTA : Sé que no es mejor usar columnas de cadena como claves primarias, así que no responda simplemente diciendo que agregue una clave primaria entera. Puedo agregar uno de todos modos, pero esta pregunta sigue siendo válida.

Rudd Zwolinski
fuente
Tengo el mismo problema y me encantaría ver una respuesta para esto. Ninguna de las sugerencias hasta ahora ha funcionado.
Sean McCleary
1
Ninguno de ellos funcionará si está utilizando Postgres. "Enterprise Rails" de Dan Chak tiene algunos consejos para usar claves naturales / compuestas.
Azeem.Butt
Tenga en cuenta que cuando use algo como: <! - language: lang-rb -> ejecute "ALTER TABLE Employees ADD PRIMARY KEY (emp_id);" Aunque la restricción de la tabla se establece correctamente después de la ejecución rake db:migrate, la definición de esquema generada automáticamente no contiene esta restricción.
pymkin

Respuestas:

107

Desafortunadamente, he determinado que no es posible hacerlo sin usar execute.

Por que no funciona

Al examinar la fuente de ActiveRecord, podemos encontrar el código para create_table:

En schema_statements.rb:

def create_table(table_name, options={})
  ...
  table_definition.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
  ...
end

Entonces podemos ver que cuando intenta especificar una clave primaria en las create_tableopciones, crea una clave primaria con ese nombre especificado (o, si no se especifica ninguno, id). Para ello, a llamar al mismo método que se puede utilizar dentro de un bloque de definición de tabla: primary_key.

En schema_statements.rb:

def primary_key(name)
  column(name, :primary_key)
end

Esto solo crea una columna con el nombre de tipo especificado :primary_key. Esto se establece en lo siguiente en los adaptadores de base de datos estándar:

PostgreSQL: "serial primary key"
MySQL: "int(11) DEFAULT NULL auto_increment PRIMARY KEY"
SQLite: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL"

La solución

Como estamos atascados con estos como tipos de clave principal, tenemos que usar executepara crear una clave principal que no sea un número entero (PostgreSQL seriales un número entero usando una secuencia):

create_table :employees, {:id => false} do |t|
  t.string :emp_id
  t.string :first_name
  t.string :last_name
end
execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"

Y como mencionó Sean McCleary , su modelo de ActiveRecord debe establecer la clave principal usando set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end
Rudd Zwolinski
fuente
15
la solución alternativa hará que rake db: test: clone o rake db: schema: load para crear emp_id como columna de entero.
Donny Kurnia
18
en realidad, ahora debería usar en self.primary_key=lugar de set_primary_key, ya que este último está en desuso.
Gary S. Weaver
2
Esta es la única solución que funcionó para mí. Espero que ayude a otros: stackoverflow.com/a/15297616/679628
findchris
8
El problema con este enfoque es que cuando configure su base de datos schema.rb emp_id, será una integercolumna de tipo nuevamente. Parece que schema.rbno se puede almacenar execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"de ninguna manera.
Tintin81
4
@DonnyKurnia @ Tintin81 Puede cambiar el volcado de esquema de schema.rba structure.sqlvia config.active_record.schema_formatconfiguración, que puede ser :sqlo :ruby. guides.rubyonrails.org/v3.2.13/…
Svilen Ivanov
21

Esto funciona:

create_table :employees, :primary_key => :emp_id do |t|
  t.string :first_name
  t.string :last_name
end
change_column :employees, :emp_id, :string

Puede que no sea bonito, pero el resultado final es exactamente lo que desea.

Austin
fuente
1
¡Finalmente! Esta es la única solución que funcionó para mí.
findchris
¿Tenemos que especificar el código para la migración hacia abajo también? ¿O es Rails lo suficientemente inteligente como para saber qué hacer cuando se realiza la migración?
amey1908
2
Para la migración descendente:drop_table :employees
Austin
6
Desafortunadamente, este método también nos deja con un schema.rbque no está sincronizado ... retendrá la :primary_key => :emp_iddeclaración, pero cuando rake db:schema:loadse llame, como está al comienzo de las pruebas, producirá una columna de entero. Sin embargo, puede darse el caso de que si cambia a usar structure.sql(opción de configuración), las pruebas retendrán la configuración y usarán ese archivo para cargar el esquema.
Tom Harrison
18

Tengo una forma de manejar esto. El SQL ejecutado es ANSI SQL, por lo que probablemente funcionará en la mayoría de las bases de datos relacionales que cumplen con ANSI SQL. He probado que esto funciona para MySQL.

Migración:

create_table :users, :id => false do |t|
    t.string :oid, :limit => 10, :null => false
    ...
end
execute "ALTER TABLE users ADD PRIMARY KEY (oid);"

En su modelo, haga esto:

class User < ActiveRecord::Base
    set_primary_key :oid
    ...
end

Sean McCleary
fuente
9

Lo he probado en Rails 4.2. Para agregar su clave principal personalizada, puede escribir su migración como:

# tracks_ migration
class CreateTracks < ActiveRecord::Migration
  def change
    create_table :tracks, :id => false do |t|
      t.primary_key :apple_id, :string, limit: 8
      t.string :artist
      t.string :label
      t.string :isrc
      t.string :vendor_id
      t.string :vendor_offer_code

      t.timestamps null: false
    end
    add_index :tracks, :label
  end
end

Mientras mira la documentación column(name, type, options = {})y lee la línea:

El typeparámetro es normalmente uno de los tipos nativos de migraciones, que es uno de los siguientes:: primary_key,: string,: text,: integer,: float,: decimal,: datetime,: time,: date,: binary,: boolean .

Obtuve los idus anteriores como he mostrado. Aquí están los metadatos de la tabla después de ejecutar esta migración:

[arup@music_track (master)]$ rails db
psql (9.2.7)
Type "help" for help.

music_track_development=# \d tracks
                    Table "public.tracks"
      Column       |            Type             | Modifiers
-------------------+-----------------------------+-----------
 apple_id          | character varying(8)        | not null
 artist            | character varying           |
 label             | character varying           |
 isrc              | character varying           |
 vendor_id         | character varying           |
 vendor_offer_code | character varying           |
 created_at        | timestamp without time zone | not null
 updated_at        | timestamp without time zone | not null
 title             | character varying           |
Indexes:
    "tracks_pkey" PRIMARY KEY, btree (apple_id)
    "index_tracks_on_label" btree (label)

music_track_development=#

Y desde la consola de Rails:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "apple_id"
>>
Arup Rakshit
fuente
3
Sin embargo, el problema es que el schema.rbarchivo que se genera no refleja el stringtipo de la clave principal, por lo que cuando se genera la base de datos con una load, la clave principal se crea como un número entero.
Peter Alfvin
9

En Rails 5 puedes hacer

create_table :employees, id: :string do |t|
  t.string :first_name
  t.string :last_name
end

Consulte la documentación de create_table .

Pavel Chuchuva
fuente
Vale la pena señalar que necesitaría agregar algún tipo de before_create :set_idmétodo en el modelo para asignar el valor de la clave principal
FloatingRock
8

Parece que es posible hacerlo usando este enfoque:

create_table :widgets, :id => false do |t|
  t.string :widget_id, :limit => 20, :primary => true

  # other column definitions
end

class Widget < ActiveRecord::Base
  set_primary_key "widget_id"
end

Eso hará que la columna widget_id sea la clave principal para la clase Widget, luego depende de usted completar el campo cuando se crean los objetos. Debería poder hacerlo utilizando la devolución de llamada antes de crear.

Así que algo parecido a

class Widget < ActiveRecord::Base
  set_primary_key "widget_id"

  before_create :init_widget_id

  private
  def init_widget_id
    self.widget_id = generate_widget_id
    # generate_widget_id represents whatever logic you are using to generate a unique id
  end
end
paulthenerd
fuente
Esto no me funciona, al menos no en PostgreSQL. No se especifica ninguna clave principal.
Rudd Zwolinski
Hmm, interesante. Lo he probado con MySQL on Rails 2.3.2. ¿Podría estar relacionado con la versión de rails también?
paulthenerd
No estoy seguro, no creo que sea la versión de Rails. Estoy usando Rails 2.3.3.
Rudd Zwolinski
Desafortunadamente, no se establece una clave principal real para la tabla que utiliza este enfoque.
Sean McCleary
2
Este enfoque funciona con Rails 4.2 y Postgresql, al menos. Lo he probado, pero necesita usar en primary_key: truelugar de solo primarydar en respuesta
Anwar
8

Estoy en Rails 2.3.5 y mi siguiente forma funciona con SQLite3

create_table :widgets, { :primary_key => :widget_id } do |t|
  t.string :widget_id

  # other column definitions
end

No es necesario: id => false.

Trung LE
fuente
lo siento, pero widget_id cambió a integer cuando aplico su técnica ... ¿tiene alguna solución?
kikicarbonell
1
Esto ya no funciona, al menos no en ActiveRecord 4.1.4. Da el siguiente error:you can't redefine the primary key column 'widgets'. To define a custom primary key, pass { id: false } to create_table.
Tamer Shlash
4

Después de casi todas las soluciones que dicen "esto funcionó para mí en la base de datos X", veo un comentario del póster original que dice "no funcionó para mí en Postgres". De hecho, el problema real aquí puede ser el soporte de Postgres en Rails, que no es impecable, y probablemente fue peor en 2009 cuando se publicó esta pregunta originalmente. Por ejemplo, si recuerdo correctamente, si estás en Postgres, básicamente no puedes obtener resultados útiles derake db:schema:dump .

Yo mismo no soy un ninja de Postgres, obtuve esta información del excelente video de Xavier Shay sobre PeepCode en Postgres. Ese video en realidad pasa por alto una biblioteca de Aaron Patterson, creo que Texticle, pero podría estar recordando mal. Pero aparte de eso, es bastante bueno.

De todos modos, si se encuentra con este problema en Postgres, vea si las soluciones funcionan en otras bases de datos. Puede usarlo rails newpara generar una nueva aplicación como sandbox, o simplemente crear algo como

sandbox:
  adapter: sqlite3
  database: db/sandbox.sqlite3
  pool: 5
  timeout: 5000

en config/database.yml.

Y si puede verificar que se trata de un problema de soporte de Postgres y encuentra una solución, contribuya con parches a Rails o empaque sus soluciones en una joya, porque la base de usuarios de Postgres dentro de la comunidad de Rails es bastante grande, principalmente gracias a Heroku. .

Giles Bowkett
fuente
2
Votación negativa por FUD e inexactitud. He usado Postgres en la mayoría de mis proyectos de Rails desde 2007. El soporte de Postgres en Rails es excelente y no hay ningún problema con el volcador de esquemas.
Marnen Laibow-Koser
2
por un lado, es cierto que Postgres no fue el problema aquí. por el otro, aquí hay un error de dumper de esquema solo pg de 2009 rails.lighthouseapp.com/projects/8994/tickets/2418 y aquí hay otro rails.lighthouseapp.com/projects/8994/tickets/2514
Giles Bowkett
Hubo un problema decidido con Rails y Postgres en torno a la actualización de Postgres 8.3 donde (correctamente, en mi opinión) decidieron cambiar al estándar SQL para literales de cadena y citas. El adaptador Rails no verificó las versiones de Postgres y tuvo que ser revisado por un tiempo.
Judson
@Judson Interesante. Nunca me encontré con eso.
Marnen Laibow-Koser
4

Encontré una solución a esto que funciona con Rails 3:

El archivo de migración:

create_table :employees, {:primary_key => :emp_id} do |t|
  t.string :emp_id
  t.string :first_name
  t.string :last_name
end

Y en el modelo employee.rb:

self.primary_key = :emp_id
shicholas
fuente
3

El truco que funcionó para mí en Rails 3 y MySQL fue este:

create_table :events, {:id => false} do |t|
  t.string :id, :null => false
end

add_index :events, :id, :unique => true

Entonces:

  1. use: id => false para no generar una clave primaria entera
  2. use el tipo de datos deseado y agregue: null => false
  3. agregar un índice único en esa columna

¡Parece que MySQL convierte el índice único en una columna no nula en una clave principal!

Clark
fuente
En Rails 3.22 con MySQL 5.5.15 crea solo una clave única, pero no una clave principal.
lulalala
2

tienes que usar la opción: id => false

create_table :employees, :id => false, :primary_key => :emp_id do |t|
    t.string :emp_id
    t.string :first_name
    t.string :last_name
end
BvuRVKyUVlViVIc7
fuente
Esto no me funciona, al menos no en PostgreSQL. No se especifica ninguna clave principal.
Rudd Zwolinski
1
Este enfoque omitirá la columna de identificación, pero la clave principal no se establecerá en la tabla.
Sean McCleary
1

¿Qué tal esta solución?

Modelo en el interior de los empleados por qué no se puede añadir código que compruebe la unicidad de Coloumn en, por ejemplo: Supongamos Empleado es el modelo en que tiene EmpId que es la cadena a continuación para que podamos añadir ": singularidad => true" a EmpId

    class Employee < ActiveRecord::Base
      validates :EmpId , :uniqueness => true
    end

No estoy seguro de que esta sea la solución, pero funcionó para mí.

Vijay Sali
fuente
1

Sé que este es un hilo antiguo con el que me topé ... pero estoy un poco sorprendido de que nadie haya mencionado DataMapper.

Creo que si necesita salirse de la convención de ActiveRecord, he descubierto que es una gran alternativa. También es un mejor enfoque para el legado y puede admitir la base de datos "tal cual".

Ruby Object Mapper (DataMapper 2) es muy prometedor y también se basa en los principios de AREL.

ingeniero Dave
fuente
1

Agregar índice funciona para mí, estoy usando MySql por cierto.

create_table :cards, {:id => false} do |t|
    t.string :id, :limit => 36
    t.string :name
    t.string :details
    t.datetime :created_date
    t.datetime :modified_date
end
add_index :cards, :id, :unique => true
Guster
fuente