Substituindo id ao criar em ActiveRecord

104

Existe alguma maneira de substituir o valor de id de um modelo na criação? Algo como:

Post.create(:id => 10, :title => 'Test')

seria ideal, mas obviamente não funcionará.

Codebeef
fonte
1
Muitas dessas respostas falharão intermitentemente com o Rails 4 e dirão que estão funcionando. Veja minha resposta para uma explicação.
Rick Smith

Respostas:

116

id é apenas attr_protected, é por isso que você não pode usar atribuição em massa para defini-lo. No entanto, ao configurá-lo manualmente, ele simplesmente funciona:

o = SomeObject.new
o.id = 8888
o.save!
o.reload.id # => 8888

Não tenho certeza de qual foi a motivação original, mas faço isso ao converter modelos ActiveHash para ActiveRecord. ActiveHash permite que você use a mesma semântica belongs_to no ActiveRecord, mas ao invés de ter uma migração e criar uma tabela, e incorrer na sobrecarga do banco de dados em cada chamada, você apenas armazena seus dados em arquivos yml. As chaves estrangeiras no banco de dados fazem referência aos ids na memória no yml.

ActiveHash é ótimo para listas de opções e pequenas tabelas que mudam com pouca frequência e apenas mudam pelos desenvolvedores. Portanto, ao passar de ActiveHash para ActiveRecord, é mais fácil apenas manter todas as referências de chave estrangeira iguais.


fonte
@jkndrkn - Não sei o que você quer dizer. Eu tenho ActiveRecord::VERSION::STRING == "3.2.11"aqui (com o adaptador sqlite3) e o acima funciona para mim.
Felix Rabe
Talvez o que experimentei apenas se expressou com o driver mysql.
jkndrkn de
@jkndrkn Funciona para mim usando o driver MySQL para Rails 3.2.18
lulalala
trabalhou para mim. salvou a minha vida. usado em rails 4.0.0 / postgres 9.3.5
allenwlee
Veja minha resposta para saber como fazer isso no Rails 4.
Rick Smith
30

Experimentar

a_post = Post.new do |p| 
  p.id = 10
  p.title = 'Test'
  p.save
end

isso deve dar a você o que você está procurando.

PJ Davis
fonte
2
não tenho certeza por que você está sendo rejeitado, isso funciona muito bem para mim
semanticart
Isso continua a funcionar a partir do registro ativo 3.2.11. A resposta postada por Jeff Dean em 2 de outubro de 2009 não funciona mais.
jkndrkn
não funciona, só parece funcionar porque chamar p.save, que provavelmente está retornando falso. obterá um ActiveRecord :: RecordNotFound se você executou p.save!
Alan,
29

Você também pode usar algo assim:

Post.create({:id => 10, :title => 'Test'}, :without_protection => true)

Embora conforme declarado nos documentos , isso contornará a segurança de atribuição em massa.

Samuel Heaney
fonte
Bom achado, @Samuel Heaney, posso verificar que funciona perfeitamente com o activerecord 3.2.13.
mkralla11
Funcionou para mim também; não foi necessário incluir um campo separado.
Shalott
2
Infelizmente, isso não funciona mais no Rails 4, eles removeram as opções de hash
Jorge Sampayo
@JorgeSampayo - no Rails 4 você não deve precisar disso, pois os atributos protegidos podem ser removidos em favor dos StrongParams.
PinnyM
21

Para Rails 4:

Post.create(:title => 'Test').update_column(:id, 10)

Outras respostas do Rails 4 não funcionaram para mim. Muitos deles pareceram mudar durante a verificação usando o Console do Rails, mas quando verifiquei os valores no banco de dados MySQL, eles permaneceram inalterados. Outras respostas só funcionaram algumas vezes.

Para MySQL, pelo menos, atribuir um idnúmero de id abaixo do auto incremento não funciona a menos que você use update_column. Por exemplo,

p = Post.create(:title => 'Test')
p.id
=> 20 # 20 was the id the auto increment gave it

p2 = Post.create(:id => 40, :title => 'Test')
p2.id
=> 40 # 40 > the next auto increment id (21) so allow it

p3 = Post.create(:id => 10, :title => 'Test')
p3.id
=> 10 # Go check your database, it may say 41.
# Assigning an id to a number below the next auto generated id will not update the db

Se você mudar createpara usar new+ save, ainda terá esse problema. Alterar manualmente o idsemelhante p.id = 10também produz esse problema.

Em geral, eu usaria update_columnpara alterar o, idmesmo que custe uma consulta de banco de dados extra porque funcionará o tempo todo. Este é um erro que pode não aparecer em seu ambiente de desenvolvimento, mas pode corromper silenciosamente seu banco de dados de produção enquanto diz que está funcionando.

Rick smith
fonte
3
Esta também foi a única maneira de fazer funcionar em uma situação de trilhos 3.2.22 no console. As respostas usando variações em 'salvar' não tiveram efeito.
JosephK
2
Para trilhos 4+ eu uso:Post.new.update(id: 10, title: 'Test')
Spencer
6

Na verdade, acontece que fazer o seguinte funciona:

p = Post.new(:id => 10, :title => 'Test')
p.save(false)
Codebeef
fonte
Embora possa funcionar, ele desativa todas as validações também, o que pode não ser o que o pedido pretendia.
Jordan Moncharmont
Isso é exatamente o que eu precisava para os dados iniciais onde os IDs importam. Obrigado.
JD.
Originalmente, votei positivamente, pensando que isso funcionaria para dados de seed, como @JD apontou, mas então tentei com activerecord 3.2.13 e ainda recebo o erro "Não é possível atribuir atributos protegidos". Então,
voto negativo
1
Infelizmente, isso não funciona no rails 4, você obtém NoMethodError: método indefinido `[] 'para false: FalseClass
Jorge Sampayo
@JorgeSampayo Isso ainda funciona se você passar validate: false, ao invés de simplesmente false. No entanto, você ainda se depara com o problema do atributo protegido - há uma maneira separada de contornar o que descrevi em minha resposta.
PinnyM
6

Como Jeff aponta, id se comporta como se fosse attr_protected. Para evitar isso, você precisa substituir a lista de atributos protegidos padrão. Tenha cuidado ao fazer isso em qualquer lugar onde as informações do atributo possam vir de fora. O campo id é protegido por padrão por um motivo.

class Post < ActiveRecord::Base

  private

  def attributes_protected_by_default
    []
  end
end

(Testado com ActiveRecord 2.3.5)

Nic Benders
fonte
6

podemos substituir attribute_protected_by_default

class Example < ActiveRecord::Base
    def self.attributes_protected_by_default
        # default is ["id", "type"]
        ["type"]
    end
end

e = Example.new(:id => 10000)
user510319
fonte
5
Post.create!(:title => "Test") { |t| t.id = 10 }

Isso não me parece o tipo de coisa que você normalmente gostaria de fazer, mas funciona muito bem se você precisar preencher uma tabela com um conjunto fixo de ids (por exemplo, ao criar padrões usando uma tarefa rake) e você deseja substituir o incremento automático (de modo que cada vez que você executar a tarefa, a tabela seja preenchida com os mesmos IDs):

post_types.each_with_index do |post_type|
  PostType.create!(:name => post_type) { |t| t.id = i + 1 }
end
Sean Cameron
fonte
2

Coloque esta função create_with_id no topo de seu seed.rb e então use-a para fazer sua criação de objeto onde ids explícitos são desejados.

def create_with_id(clazz, params)
obj = clazz.send(:new, params)
obj.id = params[:id]
obj.save!
    obj
end

e usar assim

create_with_id( Foo, {id:1,name:"My Foo",prop:"My other property"})

ao invés de usar

Foo.create({id:1,name:"My Foo",prop:"My other property"})

Darren Hicks
fonte
2

Este caso é um problema semelhante que foi necessário substituir o idpor um tipo de data personalizada:

# in app/models/calendar_block_group.rb
class CalendarBlockGroup < ActiveRecord::Base
...
 before_validation :parse_id

 def parse_id
    self.id = self.date.strftime('%d%m%Y')
 end
...
end

E depois :

CalendarBlockGroup.create!(:date => Date.today)
# => #<CalendarBlockGroup id: 27072014, date: "2014-07-27", created_at: "2014-07-27 20:41:49", updated_at: "2014-07-27 20:41:49">

Callbacks funcionam bem.

Boa sorte!.

CristianOrellanaBak
fonte
Eu precisava criar um idregistro de data e hora baseado em Unix. Eu fiz isso por dentro before_create. Funciona bem.
WM
0

Para Rails 3, a maneira mais simples de fazer isso é usar newcom o without_protectionrefinamento e então save:

Post.new({:id => 10, :title => 'Test'}, :without_protection => true).save

Para os dados iniciais, pode fazer sentido ignorar a validação, que você pode fazer assim:

Post.new({:id => 10, :title => 'Test'}, :without_protection => true).save(validate: false)

Na verdade, adicionamos um método auxiliar a ActiveRecord :: Base que é declarado imediatamente antes de executar os arquivos de semente:

class ActiveRecord::Base
  def self.seed_create(attributes)
    new(attributes, without_protection: true).save(validate: false)
  end
end

E agora:

Post.seed_create(:id => 10, :title => 'Test')

Para Rails 4, você deve usar StrongParams em vez de atributos protegidos. Nesse caso, você simplesmente poderá atribuir e salvar sem passar nenhum sinalizador para new:

Post.new(id: 10, title: 'Test').save      # optionally pass `{validate: false}`
PinnyM
fonte
Não funciona para mim sem colocar atributos {}como a resposta de Samuel acima (Rails3).
Christopher Oezbek
@PinnyM Sua resposta do Rails 4 não funciona para mim. idainda tem 10.
Rick Smith,
@RickSmith no exemplo dado, idfoi passado como 10 - então é exatamente o que deveria ser. Se não era isso que você esperava, pode esclarecer com um exemplo?
PinnyM de
Desculpe, eu quis dizer ainda não 10. Veja minha resposta para uma explicação.
Rick Smith
@RickSmith - interessante, esse problema é exclusivo do MySQL? Em qualquer caso, o uso geral para atribuir chaves primárias diretamente é para dados iniciais. Nesse caso, você geralmente NÃO deve tentar inserir valores que estão abaixo do limite de incremento automático ou deve desligar o incremento automático para esse conjunto de comandos.
PinnyM de
0

No Rails 4.2.1 com Postgresql 9.5.3, Post.create(:id => 10, :title => 'Test')funciona desde que ainda não exista uma linha com id = 10.

Nikola
fonte
0

você pode inserir id por sql:

  arr = record_line.strip.split(",")
  sql = "insert into records(id, created_at, updated_at, count, type_id, cycle, date) values(#{arr[0]},#{arr[1]},#{arr[2]},#{arr[3]},#{arr[4]},#{arr[5]},#{arr[6]})"
  ActiveRecord::Base.connection.execute sql
wxianfeng
fonte