1

I have the following models with their corresponding schema definitions:

class Cube < ApplicationRecord
  has_many :faces, inverse_of: :cube, dependent: :destroy
  accepts_nested_attributes_for :faces
end

create_table "cubes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end


##
# NOTE: Even though this model's related database table has an +id+ column, it
#       is not a regular auto-generated, auto-incremented ID, but rather an +id+
#       column that refers to an uuid that must be manually entered for each
#       Face that is created.
##
class Face < ApplicationRecord
  belongs_to :cube, inverse_of: :faces, optional: true
  validates :id, presence: true, uniqueness: true
end

# Note that id defaults to nil.
create_table "faces", id: :uuid, default: nil, force: :cascade do |t|
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.uuid "cube_id"
  t.index ["cube_id"], name: "index_faces_on_cube_id"
end

As you can see from the comments above, id is not automatically generated for the Face model, but rather is expected to be entered when they are created (because we are modeling something that in the real world already has a unique identifier, which we want to use instead of adding another ID).

The problem comes when I try to do the following:

cube = Cube.new

cube.faces_attributes = [
  {
    id: "2bfc830b-fd42-43b9-a68e-b1d98e4c99e8"
  }
]

# Throws the following error
# ActiveRecord::RecordNotFound: Couldn't find Face with ID=2bfc830b-fd42-43b9-a68e-b1d98e4c99e8 for Cube with ID=

which to me means that since we are passing an id, Rails expects this to be an update on an associated Face, instead of passing a new Face to be created and associated with the cube.

Is there a way for me to disable this default behavior by accepts_nested_attributes_for, or do I have to write custom code for my specific use case?

Daniel Olivares
  • 544
  • 4
  • 13
  • I think if you want to create a associated object in controller action, you must create it with a `new` keyword. Like - `face = Face.new` and `cube.face = face`, or something like that – Julius Dzidzevičius Nov 03 '17 at 18:33
  • @PlanB yeah, that looks like it could work. I was just asking "out of curiosity" to see if the "Convention over configuration" of nested attributes could be bent for this specific case. Thanks for contributing – Daniel Olivares Nov 03 '17 at 18:45
  • I think you better stay with `Convention over configuration`. And keeping original record id, I think is not a broblem. You can have two ids for same object because original id starts from 0 (or 1), so its not `heavy` anyway. And most important - concentrate on serious issues (while this is isnt) :) – Julius Dzidzevičius Nov 03 '17 at 18:52
  • I mean, it looks like not a serious issue, because you dont loose any data – Julius Dzidzevičius Nov 03 '17 at 18:59

1 Answers1

0

You need to configure the model to use a non-standard foreign key.

class Face < ApplicationRecord
  self.primary_key = 'uuid'
  belongs_to :cube, inverse_of: :faces, optional: true
  # your validation is for the wrong column.
  validates :uuid, presence: true, uniqueness: true
end

However in any decent database design primary keys are unique. Thats the whole point behind a unique identifier! Since NULL is not unique it must also be NOT NULL. This should be enforced on the DB level. Not just with a app level validation.

A better solution with far less headaches however might be to stick with the Rails convention of naming the foreign key id and changing its type to UUID instead of a auto-incrementing id. This will reduce the amount of configuration needed in each model considerably.

max
  • 96,212
  • 14
  • 104
  • 165