112

Is there any way of overriding a model's id value on create? Something like:

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

would be ideal, but obviously won't work.

Codebeef
  • 43,508
  • 23
  • 86
  • 119
  • 1
    Many of these answers will intermittently fail with Rails 4 and say they are working. See [my answer](http://stackoverflow.com/a/32490463/616644) for an explaination. – Rick Smith Sep 10 '15 at 16:09

13 Answers13

125

id is just attr_protected, which is why you can't use mass-assignment to set it. However, when setting it manually, it just works:

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

I'm not sure what the original motivation was, but I do this when converting ActiveHash models to ActiveRecord. ActiveHash allows you to use the same belongs_to semantics in ActiveRecord, but instead of having a migration and creating a table, and incurring the overhead of the database on every call, you just store your data in yml files. The foreign keys in the database reference the in-memory ids in the yml.

ActiveHash is great for picklists and small tables that change infrequently and only change by developers. So when going from ActiveHash to ActiveRecord, it's easiest to just keep all of the foreign key references the same.

33

You could also use something like this:

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

Although as stated in the docs, this will bypass mass-assignment security.

Samuel Heaney
  • 773
  • 1
  • 8
  • 14
31

Try

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

that should give you what you're looking for.

PJ Davis
  • 822
  • 5
  • 6
  • 2
    not sure why you're getting downvoted, this works great for me – semanticart Jul 15 '09 at 15:51
  • This continues to work as of activerecord 3.2.11. The answer posted by Jeff Dean on October 2nd 2009 no longer works. – jkndrkn Jan 30 '13 at 16:41
  • doesn't work, only seems to work because calling p.save, which is probably returning false. will get an ActiveRecord::RecordNotFound if you ran p.save! – Alan Oct 08 '14 at 19:54
21

For Rails 4:

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

Other Rails 4 answers did not work for me. Many of them appeared to change when checking using the Rails Console, but when I checked the values in MySQL database, they remained unchanged. Other answers only worked sometimes.

For MySQL at least, assigning an id below the auto increment id number does not work unless you use update_column. For example,

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

If you change create to use new + save you will still have this problem. Manually changing the id like p.id = 10 also produces this problem.

In general, I would use update_column to change the id even though it costs an extra database query because it will work all the time. This is an error that might not show up in your development environment, but can quietly corrupt your production database all the while saying it is working.

Rick Smith
  • 9,031
  • 15
  • 81
  • 85
  • 3
    This was also the only way I got it to work in a rails 3.2.22 situation in the console. The answers using variations on 'save' had no effect. – JosephK Mar 23 '16 at 13:06
  • 3
    For rails 4+ I use: `Post.new.update(id: 10, title: 'Test')` – Spencer May 03 '17 at 02:09
7

we can override attributes_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)
Remo
  • 148
  • 1
  • 7
user510319
  • 311
  • 4
  • 5
6
Post.create!(:title => "Test") { |t| t.id = 10 }

This doesn't strike me as the sort of thing that you would normally want to do, but it works quite well if you need to populate a table with a fixed set of ids (for example when creating defaults using a rake task) and you want to override auto-incrementing (so that each time you run the task the table is populate with the same ids):

post_types.each_with_index do |post_type|
  PostType.create!(:name => post_type) { |t| t.id = i + 1 }
end
Sean Cameron
  • 325
  • 3
  • 8
6

As Jeff points out, id behaves as if is attr_protected. To prevent that, you need to override the list of default protected attributes. Be careful doing this anywhere that attribute information can come from the outside. The id field is default protected for a reason.

class Post < ActiveRecord::Base

  private

  def attributes_protected_by_default
    []
  end
end

(Tested with ActiveRecord 2.3.5)

Nic Benders
  • 761
  • 7
  • 4
6

Actually, it turns out that doing the following works:

p = Post.new(:id => 10, :title => 'Test')
p.save(false)
Codebeef
  • 43,508
  • 23
  • 86
  • 119
  • While it may works, it turns off all validations as well, which may not be what the asked intended. – Jordan Moncharmont Aug 31 '12 at 17:10
  • This is exactly what I needed for seed data where IDs matter. Thank you. – JD. Nov 23 '12 at 04:58
  • I originally upvoted, thinking this would work for seed data, as @JD pointed out, but then I tried it with activerecord 3.2.13 and I still get the "Can't assign protected attributes" error. So, downvoted :( – mkralla11 Sep 11 '13 at 15:13
  • 1
    Unfortunately, this does not work in rails 4, you get NoMethodError: undefined method `[]' for false:FalseClass – Jorge Sampayo May 16 '14 at 16:16
  • @JorgeSampayo This still works if you pass `validate: false`, instead of simply `false`. However, you still run into the protected attribute issue - there is a separate way around that which I've outlined in my answer. – PinnyM Jul 02 '15 at 17:30
2

Put this create_with_id function at the top of your seeds.rb and then use it to do your object creation where explicit ids are desired.

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

and use it like this

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

instead of using

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

Darren Hicks
  • 4,946
  • 1
  • 32
  • 35
2

This case is a similar issue that was necessary overwrite the id with a kind of custom date :

# 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

And then :

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 works fine.

Good Luck!.

CristianOrellanaBak
  • 447
  • 1
  • 5
  • 13
  • I needed to create an `id` based on Unix time stamp. I have done that inside `before_create`. Works fine. – W.M. Sep 23 '17 at 20:25
0

For Rails 3, the simplest way to do this is to use new with the without_protection refinement, and then save:

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

For seed data, it may make sense to bypass validation which you can do like this:

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

We've actually added a helper method to ActiveRecord::Base that is declared immediately prior to executing seed files:

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

And now:

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

For Rails 4, you should be using StrongParams instead of protected attributes. If this is the case, you'll simply be able to assign and save without passing any flags to new:

Post.new(id: 10, title: 'Test').save      # optionally pass `{validate: false}`
PinnyM
  • 35,165
  • 3
  • 73
  • 81
  • Does not work for me without putting attributes into `{}` as Samuel's answer above (Rails3). – Christopher Oezbek Jul 28 '15 at 10:41
  • @PinnyM Your Rails 4 answer does not work for me. `id` is still 10. – Rick Smith Sep 09 '15 at 21:31
  • @RickSmith in the example given, `id` was passed as 10 - so that's exactly what it should be. If that's not what you were expecting, can you clarify with an example? – PinnyM Sep 10 '15 at 12:33
  • Sorry, I meant to say still **not** 10 . See my answer for an explanation. – Rick Smith Sep 10 '15 at 16:07
  • @RickSmith - interesting, is this problem unique to MySQL? In any case, the general usage for assigning primary keys directly is for seed data. If so, you should generally NOT be attempting to enter values that are below the autoincrement threshold, or you should turn off autoincrement for that set of commands. – PinnyM Sep 11 '15 at 15:30
  • @PinnyM Not sure if it is unique to MySQL. In theory you are right about seed data, but my inner paranoia keeps telling me if there is some use case I haven't thought of it will bite me in the worst way possible. Maybe I have PTSD? – Rick Smith Sep 11 '15 at 16:46
0

In Rails 4.2.1 with Postgresql 9.5.3, Post.create(:id => 10, :title => 'Test') works as long as there isn't a row with id = 10 already.

Nikola
  • 817
  • 13
  • 19
0

you can insert id by 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
  • 157
  • 1
  • 6