ActiveRecord Migrations : Postgres : Strings as primary keys [duplicate] - ruby-on-rails

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.

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

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?

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.

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.

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

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

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 ;)

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

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.

Related

Re-create primary key in Rails

I previously converted one of my tables' id column to bigint, and populated it with bigint values. I'm now regretting that as I can't seem to update values via ActiveRecord without runnning into RangeErrors.
I'd like to convert the id column back to the standard integer type, and clear out the existing values and populate it with auto-incremented integer values.
I don't have any foreign keys / relations I need to worry about.
Is there a straightforward way to do this via a Rails migration (Rails 4 / Postgres)?
The prior migration:
class ConvertWidgetPkToBigint < ActiveRecord::Migration
def change
change_column :widgets, :id, :bigint
end
end
Current schema.rb:
create_table "widgets", id: :bigserial, force: :cascade do |t|
...
end
Figured it out:
class RecreatePrimaryKey < ActiveRecord::Migration
def change
execute "UPDATE widgets SET id = DEFAULT"
change_column :widgets, :id, :integer
end
end
Basically it re-populates the column with auto-increment values, and then converts the column back to type integer.

In Rails, how do I create a migration that will change the type of my primary key?

I’m using Rails 4.2.7 with PostGres. I have created several tables, all of which have numeric primary keys. Below is an example of one of my Rails migrations
class CreateMyObjects < ActiveRecord::Migration
def change
create_table :my_objects do |t|
t.string :name
t.date :day
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
I do not have any data in this table, but I do have several tables that link to it through foreign keys. I want to change the primary key from being a numeric primary key to a GUID (UUID) because I’m going to have a situation where data gets created in two different databases and I don’t want there to be primary key collisions when I combine the data. How do I create a migration that will change the primary key’s type from being numeric to my UUID type and how do I update all the foreign keys that link to the table?
Thanks, - Dave
class ChangePrimaryKey < ActiveRecord::Migration
def change
remove_column :my_objects, :id # remove existing primary key
add_column :my_objects, :uuid, :string
execute "ALTER TABLE my_objects ADD PRIMARY KEY (uuid);"
end
end
class MyObject < ActiveRecord::Base
self.primary_key = :uuid
end
As to auto incrementing - no, Rails won't increment your string id for you, so you'll have to take care of it on your own.
You can use a before_create callback in the model:
before_create :generate_uuid
private
def generate_uuid
loop do
self.uuid = SecureRandom.base64(16) # or other way to generate uniq string
break unless self.class.find_by(uuid: uuid) # make sure you don't repeat the uuid
end
end
You can also add constraints to the primary_key column, like not null constraint, uniqueness, length etc

Can I change the ID to string when I work with Active Record?

It looks like the default :id is the integer, can I make it as a string?
Yes you can.
First run a migration:
create_table 'table' id: false, force: true do |t|
t.string 'id', null: false
end
specifying the type string for the id.
Then in your model:
class Table < ActiveRecord::Base
self.primary_key = :id
end
Which will basically explicitly indicate that the string id is the primary key of the object instance.
You should also consider looking up about uuid's in Rails.
Despite it being mainly a PostgreSQL piece of functionality, we've found it works well with MYSQL too:
You can set it up like this:
#app/models/post.rb
class Post < ActiveRecord::Base
before_create :set_uuid
private
def set_uuid
self.uuid = loop do
random_token = SecureRandom.hex(5)
break random_token unless self.class.exists? random_token
end
end
end
This can be accompanied - as pointed out by Cyzanfar - by replacing the id primary key with uuid. Rails 4 automatically supports uuid...
def change
create_column :posts, :uuid, :string
remove_column :posts, :id
rename_column :posts, :uuid, :id
execute "ALTER TABLE table ADD PRIMARY KEY (uuid);"
end
Some references:
Rails 4. Migrate table id to UUID
http://www.lshift.net/blog/2013/09/30/changing-the-primary-key-type-in-ruby-on-rails-models/
--
Because Rails 4 supports uuid out of the box, this should work for you.
As mentioned, we use uuid for some of our models (it allows us to maintain functionality whilst keeping records unique)

Creating custom primary keys in Rails application

In my Rails application which works with Oracle database (version 11g) I need primary keys to be Strings, not Integers. For example, in my Product model I need primary keys like "p001", "p002" etc.
class AddProductWithDifferentPrimaryKey < ActiveRecord:Migration
def change
create_table :table, id: false do |t|
t.string :id, null: false
# other columns
t.timestamps
end
execute "ALTER TABLE table ADD PRIMARY KEY (id);"
end
end
Don't forget to also add this line to your table model so rails knows how to find your new primary key!
class Product < ActiveRecord::Base
self.primary_key = :id
# rest of code
end
Hope this helps. And credit should go to
A K H
For more information you can check out his as well as other answers. primary key info

Rails: unknown attribute error for derived class

I have a model named PaypalPayment:
class PaypalPayment < PaymentMethod
belongs_to :order
def provider_class
PaypalPayment
end
def process!
end
end
I generated the following migrations for it:
class CreatePaypalPayments < ActiveRecord::Migration
def change
create_table :paypal_payments do |t|
t.integer :order_id
t.integer :payment_id
t.timestamps
end
end
end
and
class AddDetailsToPaypalPayment < ActiveRecord::Migration
def change
add_column :paypal_payments, :state, :string
add_column :paypal_payments, :amount, :decimal
add_column :paypal_payments, :cc, :string
add_column :paypal_payments, :cm, :string
end
end
After the migration the table looks something like:
development_database=# select * from paypal_payments;
id | order_id | payment_id | created_at | updated_at | state | amount | cc | cm
But when I try to initialize an object of this model, I'm getting the unknown attribute: payment_id.
#paypal_payment = PaypalPayment.new(:payment_id => params[:tx], :state => params[:st], :cc => params[:cc], :cm => params[:cm], :order_id => params[:id])
EDIT: db/schema.rb:
create_table "paypal_payments", :force => true do |t|
t.integer "order_id"
t.integer "payment_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "state"
t.decimal "amount"
t.string "cc"
t.string "cm"
end
There are different ways to model inheritance in a relational database, Martin Fowler lists the following options:
Single Table Inheritance : all classes are stored in a single table
Class Table Inheritance : all classes have their own table
Concrete Table Inheritance : only concrete classes have a table (e.g. in your example PaymentMethod if being abstract, would not have a table)
Now ActiveRecord only supports STI: single table inheritance.
So if you write
class PaypalPayment < PaymentMethod
ActiveRecord will assume STI and look for a type column, and furthermore, will only look for payment_methods table.
Depending on what you want, in most cases, STI is just perfect. Sometimes I prefer the Class and Concrete Table Inheritance better, but especially for associations this needs a little more householding, since:
e.g. you have different payment-methods, but they are stored in different tables
do you want to access all payment methods at once, you need the "abstract class"
you need an association per possible payment-method
if you have the "abstract class", how do you link to the "real payment method". One way is to include table-name and id of the child as the link.
There are lots of way to solve this, but always harder than using a single table. Also this is stretching the relational datamodel, as depending on the chosen solution, foreign key constraints are not automatically supported. I could go into detail,
but I am not sure if this is relevant, as your example seems a classic case for STI.
If you do want to use Class Table Inheritance or Concrete Table Inheritance, each class has to derive from `ActiveRecord::Base`` and you should include a module (or concern) with the shared behaviour if needed (since ruby does not support multiple inheritance).
I believe you have to add the column "type" to your PaymentMethods table. This will allow it to be inheritable. Without the type column, when you instantiate a PaypalPayment, it thinks it's a PaymentMethod and hence has none of the unique fields of PaypalPayment. However when you add the column "type" to PaymentMethod, then it will store "PaypalPayment" and ActiveRecord knows to make the PaypalPayment methods available. You should probably make a model for PaymentMethod also and make sure it inherits ActiveRecord::Base
def change
add_column :payment_methods, :type, :string
end
Here's some info:
http://www.archonsystems.com/devblog/2011/12/20/rails-single-table-inheritance-with-polymorphic-association/
I'd do this:
Check your Rails Console --
$ rails c
$ payment = PaypalPayment.find(1)
$ payment.column_names #-> should reveal which columns Rails comes back with
Check Rails is picking up the attribute
For testing's sake, just try attr_accessor :payment_id to see if that works. You might not have permitted the attribute in your model
In Rails4, that means using strong params, but in Rails 3, I think it means using attr_accessible like this:
#app/models/paypal_payment.rb
Class PaypalPayment < ActiveRecord::Base
attr_accessible :payment_id #-> tests parameter passing
attr_accessor :payment_id #-> tests virtual attribute assignment
end
I know I'm a bit late the show here, but if anyone is encountering a similar problem with Rails 5.1, in my case I was able to resolve the issue by including the following line in my parent classes
self.abstract_class = true

Resources