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:
auto_id
primary key column with AUTO_INCREMENTid
as custom column withNOT 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:
- Make the
primary_key
being auto generated by database. - 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