Utilizzando Rails, come posso impostare la mia chiave primaria in modo che non sia una colonna di tipo intero?


85

Sto usando le migrazioni di Rails per gestire uno schema di database e sto creando una semplice tabella in cui vorrei usare un valore non intero come chiave primaria (in particolare, una stringa). Per astrarre dal mio problema, supponiamo che ci sia una tabella in employeescui i dipendenti sono identificati da una stringa alfanumerica, ad es "134SNW".

Ho provato a creare la tabella in una migrazione come questa:

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

Ciò che questo mi dà è ciò che sembra che abbia completamente ignorato la riga t.string :emp_ide sia andato avanti e ne abbia fatto una colonna intera. C'è qualche altro modo per fare in modo che rails generi il vincolo PRIMARY_KEY (sto usando PostgreSQL) per me, senza dover scrivere l'SQL in una executechiamata?

NOTA : So che non è meglio usare colonne di stringhe come chiavi primarie, quindi per favore nessuna risposta dicendo solo di aggiungere una chiave primaria intera. Posso aggiungerne uno comunque, ma questa domanda è ancora valida.


Ho lo stesso problema e mi piacerebbe vedere una risposta per questo. Nessuno dei suggerimenti finora ha funzionato.
Sean McCleary

1
Nessuno di loro funzionerà se stai usando Postgres. "Enterprise Rails" di Dan Chak offre alcuni suggerimenti per l'utilizzo di chiavi naturali / composite.
Azeem.Butt

Fai attenzione che quando usi qualcosa come: <! - language: lang-rb -> esegui "ALTER TABLE dipendenti ADD PRIMARY KEY (emp_id);" Sebbene il vincolo di tabella sia impostato correttamente dopo l'esecuzione rake db:migrateLa definizione dello schema generato automaticamente non contiene questo vincolo!
pymkin

Risposte:


107

Sfortunatamente, ho stabilito che non è possibile farlo senza utilizzare execute.

Perché non funziona

Esaminando il sorgente ActiveRecord, possiamo trovare il codice per create_table:

In 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

Quindi possiamo vedere che quando provi a specificare una chiave primaria nelle create_tableopzioni, crea una chiave primaria con quel nome specificato (o, se non ne viene specificato nessuno, id). Lo fa chiamando lo stesso metodo si può utilizzare all'interno di un blocco di definizione di tabella: primary_key.

In schema_statements.rb:

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

Questo crea solo una colonna con il nome di tipo specificato :primary_key. Questo è impostato come segue negli adattatori database standard:

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

La soluzione alternativa

Dato che siamo bloccati con questi come i tipi di chiave primaria, dobbiamo usarli executeper creare una chiave primaria che non sia un numero intero (PostgreSQL serialè un numero intero usando una sequenza):

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);"

E come ha detto Sean McCleary , il tuo modello ActiveRecord dovrebbe impostare la chiave primaria utilizzando set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end

15
la soluzione alternativa renderà rake db: test: clone o rake db: schema: load per creare emp_id come colonna intera.
Donny Kurnia

18
in realtà, ora dovresti usare al self.primary_key=posto di set_primary_key, poiché quest'ultimo è deprecato.
Gary S. Weaver

2
Ecco l' unica soluzione che ha funzionato per me. Spero che aiuta gli altri: stackoverflow.com/a/15297616/679628
findchris

8
Il problema con questo approccio è che quando si imposta il database da schema.rb emp_idsarà nuovamente una integercolonna di tipo. Sembra che schema.rbnon possa memorizzare execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"in alcun modo.
Tintin81

4
@DonnyKurnia @ Tintin81 Puoi cambiare il dump dello schema da schema.rba structure.sqltramite l' config.active_record.schema_formatimpostazione, che può essere :sqlo :ruby. guide.rubyonrails.org/v3.2.13/…
Svilen Ivanov

21

Funziona:

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

Potrebbe non essere carino, ma il risultato finale è esattamente quello che vuoi.


1
Infine! Questa è l'unica soluzione che ha funzionato per me.
findchris

Dobbiamo specificare anche il codice per la migrazione verso il basso? o Rails è abbastanza intelligente da sapere cosa fare una volta migrato?
amey1908

2
Per la migrazione verso il basso:drop_table :employees
Austin

6
Sfortunatamente, questo metodo ci lascia anche con un schema.rbche non è sincronizzato ... manterrà la :primary_key => :emp_iddichiarazione, ma quando rake db:schema:loadviene chiamato, come è all'inizio dei test, produrrà una colonna intera. Tuttavia, è possibile che se si passa all'utilizzo di structure.sql(opzione di configurazione) i test manterrebbero le impostazioni e utilizzeranno quel file per caricare lo schema.
Tom Harrison

18

Ho un modo per gestirlo. L'SQL eseguito è ANSI SQL, quindi probabilmente funzionerà sulla maggior parte dei database relazionali conformi ad ANSI SQL. Ho verificato che funzioni per MySQL.

Migrazione:

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

Nel tuo modello fai questo:

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


9

L'ho provato in Rails 4.2. Per aggiungere la tua chiave primaria personalizzata, puoi scrivere la migrazione come:

# 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

Guardando la documentazione column(name, type, options = {})e leggendo la riga:

Il typeparametro è normalmente uno dei tipi nativi delle migrazioni, che è uno dei seguenti:: primary_key,: string,: text,: integer,: float,: decimal,: datetime,: time,: date,: binary,: boolean .

Ho le idee di cui sopra come ho mostrato. Ecco i metadati della tabella dopo aver eseguito questa migrazione:

[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=#

E dalla console di Rails:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "apple_id"
>>

3
Il problema è, tuttavia, che il schema.rbfile che viene generato non riflette il stringtipo per la chiave primaria, quindi quando si genera il database con a load, la chiave primaria viene creata come numero intero.
Peter Alfvin

9

In Rails 5 puoi farlo

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

Vedi la documentazione di create_table .


Vale la pena notare che dovresti aggiungere una sorta di before_create :set_idmetodo nel modello per assegnare il valore della chiave primaria
FloatingRock

8

Sembra che sia possibile utilizzare questo approccio:

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

Ciò renderà la colonna widget_id la chiave primaria per la classe Widget, quindi spetta a te popolare il campo quando vengono creati gli oggetti. Dovresti essere in grado di farlo utilizzando il callback prima di creare.

Quindi qualcosa sulla falsariga di

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

Questo non funziona per me, almeno non in PostgreSQL. Nessuna chiave primaria è specificata affatto.
Rudd Zwolinski

Hmm interessante l'ho testato con MySQL on Rails 2.3.2 - potrebbe essere correlato anche alla versione di rails forse?
paulthenerd

Non ne sono sicuro, non credo sia la versione di Rails. Sto usando Rails 2.3.3.
Rudd Zwolinski

Purtroppo una chiave primaria effettiva non è impostata per la tabella utilizzando questo approccio.
Sean McCleary

2
Questo approccio funziona almeno con Rails 4.2 e Postgresql. Ho provato, ma è necessario utilizzare primary_key: trueinvece di appena primarydato in risposta
Anwar

8

Sono su Rails 2.3.5 e il mio modo di seguire funziona con SQLite3

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

  # other column definitions
end

Non c'è bisogno di: id => false.


scusa, ma widget_id è cambiato in intero quando applico la tua tecnica ... hai una soluzione eny?
kikicarbonell

1
Questo non funziona più, almeno non in ActiveRecord 4.1.4. Fornisce il seguente errore:you can't redefine the primary key column 'widgets'. To define a custom primary key, pass { id: false } to create_table.
Tamer Shlash

4

Dopo quasi ogni soluzione che dice "questo ha funzionato per me sul database X", vedo un commento del poster originale che dice "non ha funzionato per me su Postgres". Il vero problema qui potrebbe in effetti essere il supporto di Postgres in Rails, che non è impeccabile, ed era probabilmente peggiore nel 2009 quando questa domanda è stata originariamente pubblicata. Ad esempio, se ricordo bene, se sei su Postgres, in pratica non puoi ottenere risultati utili da rake db:schema:dump.

Io non sono un ninja di Postgres, ho ricevuto queste informazioni dall'eccellente video PeepCode di Xavier Shay su Postgres. Quel video in realtà si affaccia su una libreria di Aaron Patterson, credo Texticle ma potrei ricordarmi male. Ma a parte questo è davvero fantastico.

Ad ogni modo, se stai riscontrando questo problema su Postgres, controlla se le soluzioni funzionano in altri database. Forse usalo rails newper generare una nuova app come sandbox, o semplicemente per creare qualcosa di simile

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

in config/database.yml.

E se puoi verificare che si tratta di un problema di supporto di Postgres e trovi una soluzione, per favore contribuisci con le patch a Rails o impacchetta le tue correzioni in una gemma, perché la base di utenti di Postgres all'interno della comunità di Rails è piuttosto ampia, principalmente grazie a Heroku .


2
Downvoting per FUD e imprecisione. Uso Postgres nella maggior parte dei miei progetti Rails dal 2007. Il supporto di Postgres in Rails è eccellente e non ci sono problemi con il dumper dello schema.
Marnen Laibow-Koser

2
da un lato, è vero che Postgres non era il problema qui. dall'altra, ecco un bug del dumper dello schema solo pg del 2009 rails.lighthouseapp.com/projects/8994/tickets/2418 ed eccone un altro rails.lighthouseapp.com/projects/8994/tickets/2514
Giles Bowkett

Si è verificato un problema risoluto con Rails e Postgres relativo all'aggiornamento di Postgres 8.3 in cui (correttamente, IMO) hanno deciso di passare allo standard SQL per i valori letterali di stringa e le virgolette. L'adattatore Rails non controllava le versioni di Postgres e ha dovuto essere sottoposto a monkeypatch per un po '.
Judson

@ Judson Interesting. Non mi sono mai imbattuto in quello.
Marnen Laibow-Koser

4

Ho trovato una soluzione a questo che funziona con Rails 3:

Il file di migrazione:

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

E nel modello dipendente.rb:

self.primary_key = :emp_id

3

Il trucco che ha funzionato per me su Rails 3 e MySQL è stato questo:

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

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

Così:

  1. utilizzare: id => false in modo da non generare una chiave primaria intera
  2. utilizzare il tipo di dati desiderato e aggiungere: null => false
  3. aggiungi un indice univoco su quella colonna

Sembra che MySQL converta l'indice univoco su una colonna non nulla in una chiave primaria!


In Rails 3.22 con MySQL 5.5.15 crea solo una chiave univoca, ma non una chiave primaria.
lulalala

2

devi usare l'opzione: 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

Questo non funziona per me, almeno non in PostgreSQL. Nessuna chiave primaria è specificata affatto.
Rudd Zwolinski

1
Questo approccio ometterà la colonna id ma la chiave primaria non sarà effettivamente impostata sulla tabella.
Sean McCleary

1

Che ne dici di questa soluzione,

All'interno del modello Employee, perché non possiamo aggiungere codice che verificherà l'unicità nella colonna, ad esempio: supponiamo che Employee sia Model in quanto hai EmpId che è una stringa, quindi possiamo aggiungere ": uniqueness => true" a EmpId

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

Non sono sicuro che questa sia la soluzione, ma per me ha funzionato.


1

So che questo è un vecchio thread in cui mi sono imbattuto ... ma sono un po 'scioccato che nessuno abbia menzionato DataMapper.

Trovo che se hai bisogno di allontanarti dalla convenzione di ActiveRecord, ho scoperto che è un'ottima alternativa. Inoltre è un approccio migliore per i legacy e puoi supportare il database "così com'è".

Ruby Object Mapper (DataMapper 2) mantiene molte promesse e si basa anche sui principi AREL!


1

L'aggiunta di indice funziona per me, sto usando MySql btw.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.