Usando Rails, como posso definir minha chave primária para não ser uma coluna digitada por inteiro?

86

Estou usando migrações do Rails para gerenciar um esquema de banco de dados e estou criando uma tabela simples onde gostaria de usar um valor não inteiro como a chave primária (em particular, uma string). Para abstrair do meu problema, digamos que haja uma tabela employeesonde os funcionários são identificados por uma string alfanumérica, por exemplo "134SNW".

Tentei criar a tabela em uma migração como esta:

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

O que isso me dá é o que parece ignorar completamente a linha t.string :emp_ide ir em frente e torná-la uma coluna inteira. Existe alguma outra maneira de fazer rails gerar a restrição PRIMARY_KEY (estou usando PostgreSQL) para mim, sem ter que escrever o SQL em uma executechamada?

NOTA : Eu sei que não é melhor usar colunas de string como chaves primárias, portanto, não responda apenas dizendo para adicionar uma chave primária inteira. Posso adicionar um de qualquer maneira, mas esta questão ainda é válida.

Rudd Zwolinski
fonte
Tenho o mesmo problema e adoraria ver uma resposta para isso. Nenhuma das sugestões até agora funcionou.
Sean McCleary,
1
Nenhum deles funcionará se você estiver usando Postgres. O "Enterprise Rails" de Dan Chak tem algumas dicas para usar chaves naturais / compostas.
Azeem.Butt,
Tenha cuidado ao usar algo como: <! - language: lang-rb -> execute "ALTER TABLE staff ADD PRIMARY KEY (emp_id);" Embora a restrição da tabela seja configurada corretamente após a execução rake db:migrateA definição do esquema gerado automaticamente não contém esta restrição!
pymkin

Respostas:

107

Infelizmente, determinei que não é possível fazer isso sem usar execute.

Por que não funciona

Examinando a fonte do ActiveRecord, podemos encontrar o código para create_table:

Em 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

Portanto, podemos ver que quando você tenta especificar uma chave primária nas create_tableopções, ele cria uma chave primária com esse nome especificado (ou, se nenhum for especificado, id). Ele faz isso chamando o mesmo método que você pode usar dentro de um bloco de definição de tabela: primary_key.

Em schema_statements.rb:

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

Isso apenas cria uma coluna com o nome de tipo especificado :primary_key. Isso é definido da seguinte forma nos adaptadores de banco de dados padrão:

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

A solução alternativa

Já que estamos presos a estes como os tipos de chave primária, temos que usar executepara criar uma chave primária que não seja um inteiro (o PostgreSQL serialé um inteiro usando uma sequência):

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 como Sean McCleary mencionou , seu modelo ActiveRecord deve definir a chave primária usando set_primary_key:

class Employee < ActiveRecord::Base
  set_primary_key :emp_id
  ...
end
Rudd Zwolinski
fonte
15
a solução alternativa fará o rake db: test: clone ou rake db: schema: load para criar emp_id como coluna inteira.
Donny Kurnia
18
na verdade, agora você deve usar em self.primary_key=vez de set_primary_key, já que o último está obsoleto.
Gary S. Weaver
2
Aqui está a única solução que funcionou para mim. Espero que ajude outras pessoas: stackoverflow.com/a/15297616/679628
findchris
8
O problema com essa abordagem é que, quando você configurar, seu banco de dados schema.rb emp_idserá uma integercoluna de tipo novamente. Parece que schema.rbnão pode armazenar de execute "ALTER TABLE employees ADD PRIMARY KEY (emp_id);"forma alguma.
Tintin81 de
4
@DonnyKurnia @ Tintin81 Você pode mudar o dump do esquema de schema.rbpara structure.sqlvia config.active_record.schema_formatconfiguração, que pode ser :sqlou :ruby. guias.rubyonrails.org/v3.2.13/…
Svilen Ivanov
21

Isso 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

Pode não ser bonito, mas o resultado final é exatamente o que você deseja.

Austin
fonte
1
Finalmente! Esta é a única solução que funcionou para mim.
findchris
Precisamos especificar o código para a migração também? ou o Rails é inteligente o suficiente para saber o que fazer quando migrado para baixo?
amey1908
2
Para a migração descendente:drop_table :employees
Austin
6
Infelizmente, este método também nos deixa com um schema.rbque não está em sincronia ... ele manterá a :primary_key => :emp_iddeclaração, mas quando rake db:schema:loadfor chamado, como é no início dos testes, produzirá uma coluna inteira. No entanto, pode ser o caso de que, se você alternar para o uso de structure.sql(opção de configuração), os testes retenham as configurações e usem esse arquivo para carregar o esquema.
Tom Harrison,
18

Eu tenho uma maneira de lidar com isso. O SQL executado é ANSI SQL, portanto, provavelmente funcionará na maioria dos bancos de dados relacionais compatíveis com ANSI SQL. Eu testei se isso funciona para MySQL.

Migração:

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

Em seu modelo, faça o seguinte:

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

Sean McCleary
fonte
10

No Rails 5 você pode fazer

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

Veja a documentação de create_table .

Pavel Chuchuva
fonte
É importante observar que você precisa adicionar algum tipo de before_create :set_idmétodo no modelo para atribuir o valor da chave primária
FloatingRock
9

Eu tentei no Rails 4.2. Para adicionar sua chave primária personalizada, você pode escrever sua migração 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

Enquanto olha a documentação column(name, type, options = {})e lê a linha:

O typeparâmetro é normalmente um dos tipos nativos de migração, que é um dos seguintes:: primary_key,: string,: text,: integer,: float,: decimal,: datetime,: time,: date,: binary,: boolean .

Eu tenho as ides acima como mostrei. Aqui estão os metadados da tabela após a execução desta migração:

[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 do console Rails:

Loading development environment (Rails 4.2.1)
=> Unable to load pry
>> Track.primary_key
=> "apple_id"
>>
Arup Rakshit
fonte
3
O problema é, porém, que o schema.rbarquivo gerado não reflete o stringtipo da chave primária, portanto, ao gerar o banco de dados com um load, a chave primária é criada como um inteiro.
Peter Alfvin
8

Parece que é possível fazer usando esta abordagem:

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

Isso tornará a coluna widget_id a chave primária para a classe Widget, então cabe a você preencher o campo quando os objetos forem criados. Você deve ser capaz de fazer isso usando o callback before create.

Então, algo na linha 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
Paulthenerd
fonte
Isso não funciona para mim, pelo menos não no PostgreSQL. Nenhuma chave primária é especificada.
Rudd Zwolinski
Hmm interessante, eu testei com MySQL on Rails 2.3.2 - poderia estar relacionado à versão do rails também, talvez?
paulthenerd
Não tenho certeza - não acho que seja a versão do Rails. Estou usando Rails 2.3.3.
Rudd Zwolinski
Infelizmente, uma chave primária real não é definida para a tabela usando esta abordagem.
Sean McCleary de
2
Esta abordagem funciona com Rails 4.2 e Postgresql, pelo menos. Eu testei, mas você precisa usar em primary_key: truevez de apenas primarydar uma resposta
Anwar
8

Estou no Rails 2.3.5 e minha seguinte maneira funciona com SQLite3

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

  # other column definitions
end

Não há necessidade de: id => false.

Trung LE
fonte
desculpe, mas widget_id mudou para inteiro quando aplico sua técnica ... você tem alguma solução?
enrique-carbonell de
1
Isso não funciona mais, pelo menos não no ActiveRecord 4.1.4. Ele dá o seguinte erro:you can't redefine the primary key column 'widgets'. To define a custom primary key, pass { id: false } to create_table.
Tamer Shlash
4

Depois de quase todas as soluções que dizem "isso funcionou para mim no banco de dados X", vejo um comentário do autor da postagem original no sentido de "não funcionou para mim no Postgres". O verdadeiro problema aqui pode ser o suporte Postgres em Rails, que não é perfeito, e provavelmente foi pior em 2009, quando esta questão foi postada originalmente. Por exemplo, se bem me lembro, se você está no Postgres, basicamente não consegue obter resultados úteis derake db:schema:dump .

Eu não sou um ninja do Postgres, peguei essa informação do excelente vídeo PeepCode de Xavier Shay no Postgres. Na verdade, esse vídeo ignora uma biblioteca de Aaron Patterson, acho Texticle, mas posso estar me lembrando errado. Mas, fora isso, é muito bom.

De qualquer forma, se você está tendo esse problema no Postgres, veja se as soluções funcionam em outros bancos de dados. Pode ser usado rails newpara gerar um novo aplicativo como uma sandbox ou apenas criar algo como

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

no config/database.yml.

E se você pode verificar que é um problema de suporte do Postgres, e descobrir uma correção, por favor contribua com patches para Rails ou empacote suas correções em um gem, porque a base de usuários Postgres dentro da comunidade Rails é muito grande, principalmente graças ao Heroku .

Giles Bowkett
fonte
2
Downvoting para FUD e imprecisão. Eu usei Postgres na maioria dos meus projetos Rails desde 2007. O suporte Postgres em Rails é excelente, e não há problema com o dumper de esquema.
Marnen Laibow-Koser
2
por um lado, é verdade que Postgres não era o problema aqui. por outro lado, aqui está um bug de dumper de esquema apenas pg de 2009 rails.lighthouseapp.com/projects/8994/tickets/2418 e aqui está outro rails.lighthouseapp.com/projects/8994/tickets/2514
Giles Bowkett
Houve um problema decidido com Rails e Postgres em torno da atualização Postgres 8.3 onde eles (corretamente, IMO) decidiram mudar para o padrão SQL para literais de string e citações. O adaptador Rails não verificou as versões do Postgres e teve que ser corrigido por um macaco por um tempo.
Judson
@Judson Interesting. Eu nunca encontrei isso.
Marnen Laibow-Koser
4

Eu encontrei uma solução para isso que funciona com Rails 3:

O arquivo de migração:

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

E no modelo employee.rb:

self.primary_key = :emp_id
shicholas
fonte
3

O truque que funcionou para mim no Rails 3 e no MySQL foi este:

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

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

Então:

  1. use: id => false para não gerar uma chave primária inteira
  2. use o tipo de dados desejado e adicione: null => false
  3. adicione um índice único nessa coluna

Parece que o MySQL converte o índice único de uma coluna não nula em uma chave primária!

Clark
fonte
No Rails 3.22 com MySQL 5.5.15 ele cria apenas uma chave única, mas não a chave primária.
lulalala
2

você tem que usar a opção: 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
fonte
Isso não funciona para mim, pelo menos não no PostgreSQL. Nenhuma chave primária é especificada.
Rudd Zwolinski
1
Essa abordagem omitirá a coluna id, mas a chave primária não será realmente definida na tabela.
Sean McCleary,
1

Que tal esta solução,

Dentro do modelo Employee, por que não podemos adicionar o código que verificará a exclusividade na coluna, por exemplo: Assume que Employee é o modelo em que você tem EmpId, que é string, então, para isso, podemos adicionar ": uniqueness => true" a EmpId

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

Não tenho certeza se esta é a solução, mas funcionou para mim.

Vijay Sali
fonte
1

Eu sei que este é um tópico antigo que encontrei ... mas estou meio chocado que ninguém mencionou o DataMapper.

Descobri que se você precisa se desviar da convenção do ActiveRecord, descobri que é uma ótima alternativa. Também é uma abordagem melhor para legado e você pode suportar o banco de dados "como está".

Ruby Object Mapper (DataMapper 2) é muito promissor e também se baseia nos princípios do AREL!

engenheiroDave
fonte
1

Adicionar índice funciona para mim, estou usando o 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
fonte