web-dev-qa-db-fra.com

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

J'utilise Rails migrations pour gérer un schéma de base de données, et je crée une table simple où j'aimerais utiliser une valeur non entière comme clé primaire (en particulier, un Pour résumer loin de mon problème, disons qu'il y a une table employees où 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_id et est allé de l'avant et en a fait une colonne entière. Existe-t-il une autre manière d'avoir Rails générer la contrainte PRIMARY_KEY (j'utilise PostgreSQL) pour moi, sans avoir à écrire le SQL dans un appel execute?

NOTE : 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 en disant simplement d'ajouter une clé primaire entière. Je peux quand même en ajouter un, mais cette question est toujours d'actualité.

83
Rudd Zwolinski

Malheureusement, j'ai déterminé qu'il n'est 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 le create_table options, il crée une clé primaire avec ce nom spécifié (ou, si aucun n'est spécifié, id). Pour ce faire, il appelle la même méthode que vous pouvez utiliser dans un bloc de définition de 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 spécifié de type :primary_key. Il 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

Puisque nous sommes coincés avec ceux-ci comme types de clé primaire, nous devons utiliser execute pour créer une clé primaire qui n'est pas un entier (le serial de PostgreSQL est 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 devrait définir la clé primaire en utilisant set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end
107
Rudd Zwolinski

Cela fonctionne:

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.

21
Austin

J'ai une façon de gérer cela. Le SQL exécuté est ANSI SQL, il fonctionnera donc probablement sur la plupart des bases de données relationnelles conformes à 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
18
Sean McCleary

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

# 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 paramètre type est normalement l'un des types natifs de migration, qui est l'un des suivants:: primary_key,: string,: text,: integer,: float,: decimal,: datetime,: time,: date ,: binaire,: booléen.

J'ai obtenu les ides ci-dessus comme je l'ai montré. Voici les métadonnées de la table après avoir exécuté 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 de Rails console:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "Apple_id"
>>
9
Arup Rakshit

Il semble qu'il soit possible de le 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, puis c'est à vous de remplir le champ lors de la création des objets. Vous devriez pouvoir le faire en utilisant le rappel avant de créer.

Donc, quelque chose dans le sens de

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
8
paulthenerd

Je suis sur Rails 2.3.5 et ma façon suivante fonctionne avec SQLite3

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

  # other column definitions
end

Il n'est pas nécessaire de: id => false.

8
Trung LE

Dans Rails 5 vous pouvez faire

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

Voir documentation create_table .

8
Pavel Chuchuva

Après presque toutes les solutions qui disent "cela a fonctionné pour moi sur la base de données X", je vois un commentaire de l'affiche originale à l'effet de "n'a pas fonctionné pour moi sur Postgres." Le vrai problème ici peut en fait être le support de Postgres dans Rails, qui n'est pas sans faille, 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 pas obtenir de sortie utile de rake db:schema:dump.

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

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 new pour générer une nouvelle application en tant que sandbox, 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 les correctifs à Rails ou regrouper vos correctifs dans un joyau, car la base d'utilisateurs Postgres dans le = Rails est assez grande, principalement grâce à Heroku.

4
Giles Bowkett

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
4
shicholas

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

Alors:

  1. utiliser: 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

Semble que MySQL convertit l'index unique sur une colonne non nulle en une clé primaire!

3
Clark

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
2
BvuRVKyUVlViVIc7

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
1
Guster

Que diriez-vous de cette solution,

Dans le 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, puis pour cela, nous pouvons ajouter ": unicité => vrai" à 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.

1
Vijay Sali

Je sais que c'est un vieux fil que je suis tombé sur ... mais je suis un peu choqué, personne n'a mentionné DataMapper.

Je trouve que si vous avez besoin de vous éloigner de la convention ActiveRecord, j'ai trouvé que c'est une excellente alternative. C'est également 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!

1
engineerDave