29

So I've got two models, State and Acquisition. State has_many Acquisitions. I felt like an autoincrementing integer primary key for 51 records was rather silly. So I altered the model for the State to be the PK (State being the two letter abbreviation; I'm not storing the actual state name anywhere:

class State < ActiveRecord::Base  
  self.primary_key = "state"  
  has_many :acquisition_histories  
end

The problem is when I created my Acquisition model, it created the foreign key column state_id as an integer. More specifically, the script/generated migration did:

class CreateAcquisitions < ActiveRecord::Migration  
  def self.up  
    create_table :acquisitions do |t|  
      t.date :date  
      t.string :category  
      t.text :notes  
      t.references :state  
      t.timestamps  
    end
  end
end

I'm assuming that t.references data type sets it to int. The problem is my create method on my Acquisition class is trying to put a state abbreviation into the state_id field on the table acquisitions (and yes, it's called state_id on the database, even though it says :state in the migration script). The method doesn't fail, but it does put a 0 in the state_id field and the records go into the ether.

Simon Perepelitsa
  • 20,350
  • 8
  • 55
  • 74
fr0man
  • 865
  • 3
  • 12
  • 27

9 Answers9

36

Though, I agree that this might be more trouble than it's worth considering the extra effort of working against the defaults elsewhere, just in case you actually want to do what you've asked:

Create states migration:

class CreateStatesTable < ActiveRecord::Migration  
  def change
    create_table :states, id: false do |t|
      t.string :state, limit: 2
      t.string :name
      t.index :state, unique: true
    end
  end
end

states model:

class State < ActiveRecord::Base
  self.primary_key = :state
end

Note that before Rails 3.2, this was set_primary_key = :state instead of self.primary_key= see: http://guides.rubyonrails.org/3_2_release_notes.html#active-record-deprecations

Community
  • 1
  • 1
mkirk
  • 3,965
  • 1
  • 26
  • 37
34

if you find yourself here... leave as quickly as you can and go to: Using Rails, how can I set my primary key to not be an integer-typed column?

Community
  • 1
  • 1
Wilhelm
  • 820
  • 8
  • 10
  • 3
    If you think that answer answers this question, don't post it as an answer; flag this question as a duplicate. – Nic Nov 26 '16 at 18:34
15

In Rails 5.1 you can specify the type of the primary key at creation:

create_table :states, id: :string do |t|
# ...
end

From the documentation:

A Symbol can be used to specify the type of the generated primary key column.

bloudermilk
  • 17,820
  • 15
  • 68
  • 98
  • It seems available since Rails 5.0 :) https://apidock.com/rails/v5.0.0.1/ActiveRecord/ConnectionAdapters/SchemaStatements/create_table – a2ikm Apr 10 '20 at 11:12
8

I'm working on a project that uses UUIDs as primary keys, and honestly, I don't recommend it unless you're certain you absolutely need it. There are a ton of Rails plugins out there that will not work unmodified with a database that uses strings as primary keys.

Bob Aman
  • 32,839
  • 9
  • 71
  • 95
3

Note that mkirk's answer creates a faux primary key. This explains why ActiveRecord needs to be told what the primary key is. Inspecting the table reveals

            Table "public.acquisitions"
 Column |         Type         | Modifiers
--------+----------------------+-----------
 state  | character varying(2) |
 name   | character varying    |
Indexes:
    "index_acquisitions_on_state" UNIQUE, btree (state)

In practice this works as expected so nothing wrong there, but it could be nicer.


We can keep the id column and change its type to string*. The migration looks like

class CreateAcquisitionsTable < ActiveRecord::Migration  
  def change
    create_table :acquisitions do |t|
      t.string :name
    end
    change_column :acquisitions, :id, :string, limit: 2
  end
end

Inspecting the table reveals that you have an actual primary key with all the goodies such as the unique key constraint (no unique index needed), not null constraint, and auto-incrementing key.

                                Table "public.acquisitions"
 Column |         Type         |                     Modifiers
--------+----------------------+---------------------------------------------------
 id     | character varying(2) | not null default nextval('acquisitions_id_seq'::regclass)
 name   | character varying    |
Indexes:
    "acquisitions_pkey" PRIMARY KEY, btree (id)

And you won't need to explicitly tell ActiveRecord what the primary is.

You'll want to consider setting a default id if none is provided.

class MyModel < ActiveRecord::Base
  before_create do
    self.id = SecureRandom.uuid unless self.id
  end
end

* Disclaimer: you should not change the default primary key unless you have good reason to

Community
  • 1
  • 1
Dennis
  • 56,821
  • 26
  • 143
  • 139
  • 1
    Did you check what was generated in the schema.rb file? When I tried this, the migration worked, but the schema did not contain the "string" conversion, resulting in foreign key indexes failing due to differing column types. – Andreas Jul 29 '16 at 12:59
2

You want to follow the Rails conventions. The extra primary key is not an issue in any way. Just use it.

August Lilleaas
  • 54,010
  • 13
  • 102
  • 111
  • 7
    Storage cost (obvious). Computational cost (clustering + autoincrement IDs isn't necessarily good). Complexity cost (schema is confusing if it has noise over what really needs to be there). Using UUIDs as primary keys make a whole lot of sense in offline applications that must correlate their data later with a central DB) – sethcall May 13 '12 at 14:36
1
class CreateAcquisitions < ActiveRecord::Migration  
    def self.up  
        create_table :acquisitions, :id => false do |t|  
          t.date :date  
          t.string :category  
          t.text :notes  
          t.references :state  
          t.timestamps
        end
    end
end
jvperrin
  • 3,368
  • 1
  • 23
  • 33
todd
  • 2,381
  • 1
  • 20
  • 11
1

I had a bit of experience with string used as primary keys and it's a pain in the ***. Remember that by default if you want to pass an object with the default :controller/:action/:id pattern, the :id will be a string and this will probably lead to routing problems if some ids get weirdly formatted ;)

-28

Rails works best when you don't fight against the defaults. What harm does it do to have an integer primary key on your state table?

Unless you're stuck with a legacy schema that you have no control over, I'd advise you to stick to the Rails defaults—convention over configuration, right?—and concentrate on the important parts of your app, such as the UI and the business logic.

John Topley
  • 113,588
  • 46
  • 195
  • 237
  • Well, there's a (probably memory-cached and tiny) database overhead when you have to keep including the state table to get the code. But it's probably over-optimisation. I do that, but I'm old and it used to matter more in VAX 8700 days. – Mike Woodhouse Apr 15 '09 at 08:38
  • 2
    For that sort of thing I tend to define a constant in the State class i.e. ALL = State.all and then use State::ALL as the source for dropdowns etc. That way, the SQL query only runs at start-up in Production. – John Topley Apr 15 '09 at 09:27
  • 1
    I'm not worried about performance at all, I just don't like the concept of an extra unique identifier. But I'm going to go back with the convention, assuming I can figure out how to back everything back out. I'm fairly new to Rails. – fr0man Apr 16 '09 at 02:37
  • 28
    Unfortunately, there are actually use cases in which you might want to do something out of the box. For the 99 the convention may be ideal but in the off cases a more helpful response might be warranted. – JC Grubbs May 24 '12 at 19:14
  • 1
    In which case Rails may not be the best fit. – John Topley May 25 '12 at 14:25
  • 10
    I agree with @JohnTopley that it is best not to fight Rails, but the answer doesn't actually answer the question. I think it is possible to achieve with Rails (but don't yet know how either). There are plenty of legitimate reasons for not being able to use an auto-incrementing integer for primary keys (like scalability requirements forcing distributed implementations that cannot have any centralized components at all, for example). – DavidJ Aug 29 '12 at 21:39
  • Sequentially incrementing primary keys isn't technically a problem - If you don't expose the key to anyone. As soon as you expose it, you're subject to enumeration attacks. – ekampp May 12 '22 at 13:43