Home > OS >  Rails Model - custom primary key with ID as a custom column
Rails Model - custom primary key with ID as a custom column

Time:07-20

I struggle with modeling my table to Rails model with custom primary key and id as a regular (not null) column.

The problem is my table has:

  1. auto_id primary key column with AUTO_INCREMENT
  2. id as custom column with NOT NULL constraint on it (it should be set by the application side before saving)
class InitialMigration < ActiveRecord::Migration[6.1]
  def change
    create_table :cars, id: false, force: true do |t|
      t.primary_key :auto_id
      t.string :id, null: false
    end
  end
end

I want to:

  1. Make the primary_key being auto generated by database.
  2. Set the id value directly in the application before saving the model.

But when I try to save a Car instance to database I have a problem with setting id from the app.

class Car < ActiveRecord::Base
  self.primary_key = "auto_id"

  before_create do
    binding.pry
  end
end

Even in the binding.pry line, when I call self.id = '1234' it's being reassigned to auto_id field, instead of id.

Thus id columns always remain NULL which leads to a DB error Field 'id' doesn't have a default value.

[2] pry(#<Car>)> self.id = '9'
=> "9"
[3] pry(#<Car>)> self
=> #<Car:0x00007fcdb34ebe60
 auto_id: 9,
 id: nil>

PS. It's Rails 6.

CodePudding user response:

I'd avoid using id this way by renaming it to something else. In rails id is whatever the @primary_key is set to. There is a whole module dedicated to it, that defines id attribute methods:

https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/PrimaryKey.html

These methods can be overridden, but shouldn't:

class Car < ApplicationRecord
  self.primary_key = "auto_id"

  def id= value
    _write_attribute("id", value)
  end

  def id
    _read_attribute("id")
  end
end

"id" is hardcoded in write_attribute to use primary_key, so we can't use write_attribute to set id attribute itself.

Also, id methods are used in other places. Overriding them is a bad idea.

>> Car.create(id: "99")
=> #<Car:0x00007f723e801788 auto_id: nil, id: "99">
#                                    ^
# NOTE: Even though car has been saved, `auto_id` is still `nil`
#       but only in `car` object. Because we have overridden
#       `id`, primary key is read from `id` column and then `id` 
#       attribute is set in `id=`. `auto_id` is bypassed.

>> Car.last
=> #<Car:0x00007f774d9e8dd0 auto_id: 1, id: "99">

# NOTE: Other methods are broken whenever `id` is used

>> Car.last.to_key
=> ["99"]
>> Car.last.to_gid
=> #<GlobalID:0x00007f774f4def68 @uri=#<URI::GID gid://stackoverflow/Car/99>>

A better way is to not touch id methods:

class Car < ApplicationRecord
  self.primary_key = "auto_id"

  def id_attribute= value
    _write_attribute("id", value)
  end

  def id_attribute
    _read_attribute("id")
  end
end
>> car = Car.create(id_attribute: "99")
=> #<Car:0x00007fb1e44d9458 auto_id: 2, id: "99">

>> car.id_attribute
=> "99"
>> car.id
=> 2
>> car.auto_id
=> 2
  • Related