58

Is there a compact way with ActiveRecord to query for what id it's going to use next if an object was going to be persisted to the database? In SQL, a query like this would look something like:

SELECT max(id) + 1 FROM some_table;
Pang
  • 9,564
  • 146
  • 81
  • 122
randombits
  • 47,058
  • 76
  • 251
  • 433
  • 2
    This depends on the database you're using, and if records have been deleted..... EXAMPLE: delete the last 1000 records in a postgresql table and do an insert. The id is not the max + 1. In a paradox DB the next record could get an earlier id, because it re-uses them. It really really depends on your DB. – baash05 Feb 18 '13 at 21:58
  • Also... if you have two people logged in and both are entering products/purchases_orders/jobs/events. This would return the same ID for both of them. – baash05 Feb 18 '13 at 22:03
  • 3
    The simple answer is you don't do it like that, you create an id generator/provider model. – ocodo Sep 24 '13 at 06:58

14 Answers14

113

Here is a slightly modified Taryn East's version:

Model.maximum(:id).next
# or
Model.calculate(:maximum, :id).next

Sources:

#maximum @ ActiveRecord::Calculations

#next @ Integer

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
kimerseen
  • 2,511
  • 2
  • 16
  • 5
  • 26
    This will give an exception if there are no rows present(undefined next for nil). I suggest an improvement: `Model.maximum(:id).to_i.next` – Rustam Gasanov Jan 16 '14 at 12:09
  • 3
    @RustamA.Gasanov your solution will produce the wrong id especially if your id is Alphanumeric (e.g C10293029). Its safer to assume you will be getting a nil and check for it before proceeding to do a .next or return a default value. I do think this is a safer solution than Taryn's answer – Ransom Feb 04 '14 at 12:08
  • 6
    Ok, but what if the last record has been destroyed? `User.maximum(:id).next` returns 27, after `User.last.destroy` its 26, but now - new record has id 27. I don't think it's a 100% safe way to predict id – res Aug 27 '15 at 09:01
  • 1
    @res assuming a boilerplate setup and using `id` as the primary key, the new record would have an id of `28`, however, `User.maximum(:id).next` would still return `27` because the `id` isn't recycled. @Les Nightingill posted a more accurate method below, although one could still run into issues with the ID being set by another action before it's assigned. – Eric Norcross Jan 18 '17 at 05:40
  • As others have stated, this answer doesn't work if you delete the last record. – 3pns Aug 20 '18 at 06:45
  • 1
    This should definitely not be used for determining next incrementing `id`, an `auto-increment` should be used for that, but this does work great for something like a `sort_order` column, where sequential integrity doesn't matter as much. As long as the determined `sort_order` is larger than the last one, it doesn't matter if a previous one was deleted. Given the existing `sort_order` values of `[1, 2, 3]`, next determination will result in `[1, 2, 3, 4]`. `[1, 3]` will result in `[1, 3, 4]`, etc. `[]` will trigger an error, unless `.to_i` is included, then will result in `[1]`, so from me! – Tim Lewis Nov 23 '22 at 16:28
32

While accepting fig's answer I might want to draw your attention to a small thing. If you are getting the next ID to set to a particular record before saving, I think its not a good idea.

because as an example in a web based system

  1. you get the last id as 10
  2. you set the next id as 11
  3. before you save the record someone else has saved the record, now the last id should be 12 likewise..

I'm not sure you want the last id to do what I'm thinking here, But if so this is just to draw your attention.

Community
  • 1
  • 1
sameera207
  • 16,547
  • 19
  • 87
  • 152
30

If your database is Postgres, you can get the next id with this (example for a table called 'users'):

ActiveRecord::Base.connection.execute("select last_value from users_id_seq").first["last_value"]

Unlike the other answers, this value is not affected by the deletion of records.

There's probably a mySQL equivalent, but I don't have one set up to confirm.

If you have imported data into your postgresql database, there's a good chance that the next id value after the import is not set to the next integer greater than the largest one you imported. So you will run into problems trying to save the activerecord model instances.

In this scenario, you will need to set the next id value manually like this:

ActiveRecord::Base.connection.execute("alter sequence users_id_seq restart with 54321;") #or whatever value you need
Les Nightingill
  • 5,662
  • 1
  • 29
  • 32
  • 2
    The MySQL equivalent is `ActiveRecord::Base.connection.execute("SELECT Auto_increment FROM information_schema.tables WHERE table_name=''").first.first` (via [this SO answer](https://stackoverflow.com/a/1405500/4865822)) – Ryan Lue Jan 30 '19 at 11:42
  • responding to the user (I don't know the name) who tried to edit this answer... The method indicated correctly returns the value 1 when the table is empty. The intention is that the method should return the next value that will be assigned. When the table is empty, the next value is 1. If you disagree, by all means add a comment and I'll look at your reasoning. – Les Nightingill Sep 28 '19 at 01:22
13

Slightly better than the accepted answer:

YourModel.maximum(:id) + 1

Still prone to race-conditions etc, but at least it will take note of skipped ids and is slightly more efficient than, say, ordering the table by id then returning the last.

Taryn East
  • 27,486
  • 9
  • 86
  • 108
  • 1
    this is not reliable. If the model with the highest id (n) had been deleted, then this solution will return the value n-1+1, or just n. But the next id that will be assigned is actually n+1. – Les Nightingill Feb 23 '16 at 17:14
  • Yes - as noted, this is not perfect, just better than the accepted answer :) It will consistently return an id that is not taken. – Taryn East Feb 23 '16 at 22:09
  • 1
    Since `YourModel.maximum(:id)` returns `nil` for an empty table, you might like to: `(YourModel.maximum(:id) || 0) + 1`. Unlikely to be a problem in an established production system, but might be more robust in a new or test environment. – David Aldridge Aug 27 '17 at 09:00
11

This is an old question, but none of the other answers work if you have deleted the last record:

Model.last.id #=> 10
Model.last.destroy
Model.last.id #=> 9, so (Model.last.id + 1) would be 10... but...
Model.create  #=> 11, your next id was actually 11

I solved the problem using the following approach:

current_value = ActiveRecord::Base.connection.execute("SELECT currval('models_id_seq')").first['currval'].to_i
Model.last.id #=> 10
Model.last.destroy
Model.last.id #=> 9
current_value + 1 #=> 11
gabrielhilal
  • 10,660
  • 6
  • 54
  • 81
4

Get the largest id on the table

YourModel.maximum(:id)

This will run the following sql

SELECT MAX("your_models"."id") AS max_id FROM "your_models"

Convert the result to an integer by calling to_i. This is important as for an empty table the above max command will return nil. Fortunately nil.to_i returns 0.

Now to get the next available id, just add 1 +1`

The final result:

YourModal.maximum(:id).to_i+1
user566245
  • 4,011
  • 1
  • 30
  • 36
3

If no one else is using the table (otherwise you would have to use locking), you could get the autoincrement value from MySQL, the SQL command is

SELECT auto_increment FROM information_schema.tables 
WHERE table_schema = 'db_name' AND table_name = 'table_name';

and the Rails command would be

ActiveRecord::Base.connection.execute("SELECT auto_increment 
     FROM information_schema.tables 
     WHERE table_schema = 'db_name' AND table_name = 'table_name';").first[0]
0x4a6f4672
  • 27,297
  • 17
  • 103
  • 140
2

If you want to be sure no one else can take the 'new' index, you should lock the table. By using something like:

ActiveRecord::Base.connection.execute("LOCK TABLES table_name WRITE")

and

ActiveRecord::Base.connection.execute("UNLOCK TABLES")

But this is specific for each database engine.

The only correct answer for a sequential id column is:

YourModel.maximum(:id)+1

If you sort your model in a default scope, last and first will depend on that order.

  • In empty tables `YourModel.maximum(:id)` will give nil. Then `nil + 1` will give error – user566245 Oct 14 '14 at 14:21
  • incase of nil you could make it so `self.maximum(:id) ? self.maximum(:id) + 1 : 1` or another way you safe it in case of nil – buncis Feb 09 '19 at 15:08
2

I don't think there's a generic answer to this since you may not want to assume one database over another. Oracle, for example, may assign id's by a sequence or, worse, by a trigger.

As far as other databases are concerned, one may set them up with non sequential or random id allocation.

May I ask what the use case is? Why would you need to anticipate the next id? What does 'next' mean? Relative to when? What about race conditions (multiuser environments)?

dynex
  • 905
  • 7
  • 8
1

There seems to be need of a answer here that removes the race conditions, and addresses the actual problem of pre-providing unique identifiers.

To solve the problem you maintain a second model, which serves unique ID's for the primary model. This way you don't have any race condition.

If you need to make these ID's secure you should hash them with SHA256 (or SHA512) and store the hash as an indexed column on identifier model when they are generated.

They can then be associated and validated when used on the primary model. If you don't hash them you still associate them, to provide validation.

I'll post some example code a bit later.

ocodo
  • 29,401
  • 18
  • 105
  • 117
  • ocodo, did you ever manage to post this code anywhere? – sandre89 Feb 11 '22 at 20:03
  • That was a long time ago. The concept here is known as a version vector or vector clock. Lots of stuff written about them. They're used for a variety of things in distributed systems where race conditions are rife. – ocodo Feb 12 '22 at 02:44
  • @sandre89 fwiw I don't know of an ActiveRecord solution for this. Personally I would recommend solving the specific problem you have with another method. – ocodo Feb 12 '22 at 02:47
0

Don't get the intent but you might wanna think of using GUID

Ryo
  • 2,003
  • 4
  • 27
  • 42
0

You should never assume what the next id will be in the sequence. If you have more than 1 user you run the risk of the id being in use by the time the new object is created.

Instead, the safe approach would be to create the new object and update it. Making 2 hits to your database but with an absolute id for the object you're working with.

This method takes the guess work out of the equation.

AnthonyM.
  • 339
  • 2
  • 5
0

I came to this SO question because I wanted to be able to predict the id of a model created in my test suite (the id was then used a REST request to an external service and I needed to predict the exact value to mock the request).

I found that Model.maximum(:id).next, although elegant, doesn't work in a rails testing environment with transactional fixtures since there are usually no records in the db so it will simply return nil.

Transactional fixtures make the issue extra tricky since the auto increment field ascends even when there aren't any records in the db. Furthermore using an ALTER TABLE ***your_table_name*** AUTO_INCREMENT = 100 breaks the transaction your tests are in because it requires its own transaction.

What I did to solve this was to create a new object and add 1 to its id:

let!(:my_model_next_id) { FactoryBot.create(:my_model).id + 1 }

Although somewhat hacky (and slightly inefficient on your db since you create an extra object for the sake of its id), it doesn't do anything goofy to the transaction and works reliably in a testing environment with no records (unless your tests run in parallel with access to the same db...in which case: race conditions...maybe?).

Jacob Dalton
  • 1,643
  • 14
  • 23
0

Along with above answers, here is another way to check current auto increment:

query = "show create table features" 
res = ActiveRecord::Base.connection.execute(query)
puts res.to_a 

This can also print current AUTO_INCREMENT property, along with table properties such as create table statement, CHARSET etc..