À l'aide de Rails, comment puis-je définir ma clé primaire pour qu'elle ne soit pas une colonne de type entier?

85

J'utilise les migrations Rails pour gérer un schéma de base de données, et je crée une table simple dans laquelle je voudrais utiliser une valeur non entière comme clé primaire (en particulier, une chaîne). Pour résumer mon problème, disons qu'il existe un tableau employeesoù les employés sont identifiés par une chaîne alphanumérique, par exemple "134SNW".

J'ai essayé de créer la table dans une migration comme celle-ci:

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

Ce que cela me donne, c'est ce qui semble avoir complètement ignoré la ligne t.string :emp_idet est allé de l'avant et en a fait une colonne entière. Existe-t-il un autre moyen pour que les rails génèrent la contrainte PRIMARY_KEY (j'utilise PostgreSQL) pour moi, sans avoir à écrire le SQL dans un executeappel?

REMARQUE : Je sais qu'il n'est pas préférable d'utiliser des colonnes de chaînes comme clés primaires, donc s'il vous plaît pas de réponses disant simplement d'ajouter une clé primaire entière. Je peux quand même en ajouter un, mais cette question est toujours d'actualité.

Rudd Zwolinski
la source
J'ai le même problème et j'aimerais voir une réponse à cela. Aucune des suggestions jusqu'à présent n'a fonctionné.
Sean McCleary
1
Aucun d'entre eux ne fonctionnera si vous utilisez Postgres. "Enterprise Rails" de Dan Chak a quelques astuces pour utiliser des clés naturelles / composites.
Azeem.Butt le
Attention, lorsque vous utilisez quelque chose comme: <! - language: lang-rb -> exécutez "ALTER TABLE employés ADD PRIMARY KEY (emp_id);" Bien que la contrainte de table soit correctement définie après l'exécution rake db:migrateLa définition de schéma générée automatiquement ne contient pas cette contrainte!
pymkin

Réponses:

107

Malheureusement, j'ai déterminé qu'il n'était pas possible de le faire sans utiliser execute.

Pourquoi ça ne marche pas

En examinant la source ActiveRecord, nous pouvons trouver le code pour create_table:

Dans 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

Nous pouvons donc voir que lorsque vous essayez de spécifier une clé primaire dans les create_tableoptions, cela crée une clé primaire avec ce nom spécifié (ou, si aucune n'est spécifiée, id). Elle le fait en appelant la même méthode que vous pouvez utiliser à l' intérieur d' un bloc de définition de la table: primary_key.

Dans schema_statements.rb:

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

Cela crée simplement une colonne avec le nom de type spécifié :primary_key. Ceci est défini comme suit dans les adaptateurs de base de données standard:

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

La solution de contournement

Puisque nous sommes bloqués avec ceux-ci comme types de clés primaires, nous devons utiliser executepour créer une clé primaire qui n'est pas un entier (PostgreSQL serialest un entier utilisant une séquence):

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

Et comme Sean McCleary l'a mentionné , votre modèle ActiveRecord doit définir la clé primaire en utilisant set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end
Rudd Zwolinski
la source
15
la solution de contournement rendra le rake db: test: clone ou rake db: schema: load pour créer emp_id en tant que colonne entière.
Donny Kurnia
18
en fait, vous devriez maintenant utiliser à la self.primary_key=place de set_primary_key, car ce dernier est obsolète.
Gary S. Weaver
2
Voici la seule solution qui a fonctionné pour moi. J'espère que cela aidera les autres: stackoverflow.com/a/15297616/679628
findchris
8
Le problème avec cette approche est que lorsque vous configurez votre base de données à partir de schema.rb emp_idsera à nouveau une integercolonne de type. Il semble que schema.rbcela ne peut execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"en aucun cas stocker .
Tintin81
4
@DonnyKurnia @ Tintin81 Vous pouvez basculer le vidage de schéma de schema.rbà structure.sqlvia le config.active_record.schema_formatparamètre, qui peut être soit :sqlou :ruby. guides.rubyonrails.org/v3.2.13/…
Svilen Ivanov
21

Cela marche:

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

Ce n'est peut-être pas joli, mais le résultat final est exactement ce que vous voulez.

Austin
la source
1
Finalement! C'est la seule solution qui a fonctionné pour moi.
findchris
Doit-on également spécifier du code pour la migration vers le bas? ou Rails est-il suffisamment intelligent pour savoir quoi faire lors de la migration vers le bas?
amey1908
2
Pour la migration descendante:drop_table :employees
Austin
6
Malheureusement, cette méthode nous laisse aussi avec un schema.rbqui n'est pas synchronisé ... elle conservera la :primary_key => :emp_iddéclaration, mais lorsqu'elle rake db:schema:loadest appelée, comme c'est le cas au début des tests, elle produira une colonne entière. Cependant, il se peut que si vous passez à l'utilisation structure.sql(option de configuration), les tests conservent les paramètres et utilisent ce fichier pour charger le schéma.
Tom Harrison
18

J'ai une façon de gérer cela. Le SQL exécuté est du SQL ANSI, il fonctionnera donc probablement sur la plupart des bases de données relationnelles compatibles ANSI SQL. J'ai testé que cela fonctionne pour MySQL.

Migration:

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

Dans votre modèle, procédez comme suit:

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

Sean McCleary
la source
9

Je l'ai essayé dans Rails 4.2. Pour ajouter votre clé primaire personnalisée, vous pouvez écrire votre migration sous la forme:

# 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

En regardant la documentation de column(name, type, options = {})et lisez la ligne:

Le typeparamètre est normalement l'un des types natifs des migrations, qui est l'un des suivants:: primary_key,: string,: text,: integer,: float,: decimal,: datetime,: time,: date,: binary,: boolean .

J'ai eu les ides ci-dessus comme je l'ai montré. Voici les métadonnées du tableau après l'exécution de cette migration:

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

Et depuis la console Rails:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "apple_id"
>>
Arup Rakshit
la source
3
Le problème vient, cependant, que le schema.rbfichier qui est généré ne reflète pas le stringtype de la clé primaire, donc lors de la génération de la base de données avec a load, la clé primaire est créée sous forme d'entier.
Peter Alfvin
9

Dans Rails 5, vous pouvez faire

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

Voir la documentation de create_table .

Pavel Chuchuva
la source
Il convient de noter que vous devez ajouter une sorte de before_create :set_idméthode dans le modèle pour attribuer la valeur de la clé primaire
FloatingRock
8

Il semble qu'il est possible de faire en utilisant cette approche:

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

Cela fera de la colonne widget_id la clé primaire de la classe Widget, alors c'est à vous de remplir le champ lors de la création des objets. Vous devriez pouvoir le faire en utilisant le callback before create.

Donc, quelque chose du genre

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
la source
Cela ne fonctionne pas pour moi, du moins pas dans PostgreSQL. Aucune clé primaire n'est spécifiée du tout.
Rudd Zwolinski le
Hmm intéressant, je l'ai testé avec MySQL sur Rails 2.3.2 - pourrait être lié à la version des rails peut-être aussi?
paulthenerd
Je ne suis pas sûr - je ne pense pas que ce soit la version de Rails. J'utilise Rails 2.3.3.
Rudd Zwolinski le
Malheureusement, une clé primaire réelle n'est pas définie pour la table à l'aide de cette approche.
Sean McCleary le
2
Cette approche fonctionne au moins avec Rails 4.2 et Postgresql. J'ai testé, mais vous devez utiliser primary_key: trueau lieu de simplement primarydonner en réponse
Anwar
8

Je suis sur Rails 2.3.5 et ma méthode suivante fonctionne avec SQLite3

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

  # other column definitions
end

Il n'y a pas besoin de: id => false.

Trung LE
la source
désolé, mais widget_id a changé en entier quand j'applique votre technique ... avez-vous une solution eny?
kikicarbonell
1
Cela ne fonctionne plus, du moins pas dans ActiveRecord 4.1.4. Il donne l'erreur suivante:you can't redefine the primary key column 'widgets'. To define a custom primary key, pass { id: false } to create_table.
Tamer Shlash
4

Après presque chaque solution qui dit "cela a fonctionné pour moi sur la base de données X", je vois un commentaire de l'affiche originale indiquant que "n'a pas fonctionné pour moi sur Postgres". Le vrai problème ici peut en fait être le support Postgres dans Rails, qui n'est pas parfait, et était probablement pire en 2009 lorsque cette question a été publiée à l'origine. Par exemple, si je me souviens bien, si vous êtes sur Postgres, vous ne pouvez pratiquement pas obtenir de sortie utile rake db:schema:dump.

Je ne suis pas un ninja de Postgres moi-même, j'ai obtenu cette information de l'excellente vidéo PeepCode de Xavier Shay sur Postgres. Cette vidéo donne en fait sur une bibliothèque d'Aaron Patterson, je pense que Texticle mais je me souviens peut-être mal. Mais à part ça, c'est plutôt génial.

Quoi qu'il en soit, si vous rencontrez ce problème sur Postgres, voyez si les solutions fonctionnent dans d'autres bases de données. Peut-être utiliser rails newpour générer une nouvelle application en tant que bac à sable, ou simplement créer quelque chose comme

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

dans config/database.yml.

Et si vous pouvez vérifier qu'il s'agit d'un problème de support Postgres et que vous trouvez un correctif, veuillez contribuer aux correctifs à Rails ou empaqueter vos correctifs dans un joyau, car la base d'utilisateurs Postgres au sein de la communauté Rails est assez importante, principalement grâce à Heroku .

Giles Bowkett
la source
2
Vote négatif pour FUD et inexactitude. J'ai utilisé Postgres sur la plupart de mes projets Rails depuis 2007. Le support de Postgres dans Rails est excellent, et il n'y a aucun problème avec le dumper de schéma.
Marnen Laibow-Koser
2
d'une part, il est vrai que Postgres n'était pas le problème ici. de l'autre, voici un bug de dumper de schéma pg uniquement de 2009 rails.lighthouseapp.com/projects/8994/tickets/2418 et en voici un autre rails.lighthouseapp.com/projects/8994/tickets/2514
Giles Bowkett
Il y avait un problème résolu avec Rails et Postgres concernant la mise à jour Postgres 8.3 où ils (correctement, IMO) ont décidé de passer au standard SQL pour les littéraux de chaîne et les guillemets. L'adaptateur Rails n'a pas vérifié les versions de Postgres et a dû être monkeypatched pendant un certain temps.
Judson
@Judson Intéressant. Je n'ai jamais rencontré ça.
Marnen Laibow-Koser
4

J'ai trouvé une solution à cela qui fonctionne avec Rails 3:

Le fichier de migration:

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

Et dans le modèle employee.rb:

self.primary_key = :emp_id
Shicholas
la source
3

L'astuce qui a fonctionné pour moi sur Rails 3 et MySQL était la suivante:

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

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

Donc:

  1. utilisez: id => false pour ne pas générer de clé primaire entière
  2. utilisez le type de données souhaité et ajoutez: null => false
  3. ajouter un index unique sur cette colonne

Il semble que MySQL convertit l'index unique d'une colonne non nulle en clé primaire!

Clark
la source
Dans Rails 3.22 avec MySQL 5.5.15, il crée une clé unique uniquement, mais pas une clé primaire.
lulalala
2

vous devez utiliser l'option: 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
la source
Cela ne fonctionne pas pour moi, du moins pas dans PostgreSQL. Aucune clé primaire n'est spécifiée du tout.
Rudd Zwolinski le
1
Cette approche omettra la colonne id mais la clé primaire ne sera pas réellement définie sur la table.
Sean McCleary
1

Et cette solution,

À l'intérieur du modèle Employee, pourquoi ne pouvons-nous pas ajouter du code qui vérifiera l'unicité dans coloumn, par exemple: Supposons que l'employé est un modèle en ce que vous avez EmpId qui est une chaîne alors pour cela, nous pouvons ajouter ": uniqueness => true" à EmpId

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

Je ne suis pas sûr que ce soit une solution, mais cela a fonctionné pour moi.

Vijay Sali
la source
1

Je sais que c'est un vieux fil sur lequel je suis tombé par hasard ... mais je suis un peu choqué que personne n'ait mentionné DataMapper.

Je trouve que si vous devez vous éloigner de la convention ActiveRecord, j'ai trouvé que c'était une excellente alternative. C'est aussi une meilleure approche pour l'héritage et vous pouvez prendre en charge la base de données "telle quelle".

Ruby Object Mapper (DataMapper 2) est très prometteur et s'appuie également sur les principes AREL!

ingénieur Dave
la source
1

L'ajout d'index fonctionne pour moi, j'utilise 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
Guster
la source