I'm trying to build a JobReport model that holds the return value from GoodJob jobs. The two fields I could build an association on, id and active_job_id, are problematic. The id field is set to return the active_job_id in the Job class:
# from good_job-3.12.1/app/models/good_job/job.rb
def id
active_job_id
end
The good_jobs.active_job_id field has no uniqueness constraint, and setting it as a foreign key throws a postgres error.
How can I link these two tables?
Here's the migration I'm using to create the job_reports table:
class CreateJobReports < ActiveRecord::Migration[7.0]
def change
create_table :job_reports do |t|
t.text :report
t.uuid :good_job_id
t.timestamps
end
add_foreign_key :job_reports, :good_jobs, column: :good_job_id, primary_key: :id
end
end
My JobReport model:
class JobReport < ApplicationRecord
belongs_to :good_job, class_name: 'GoodJob::Job', foreign_key: 'id'
end
And my good_job.rb initializer contains:
GoodJob::Job.class_eval do
has_one :job_report, dependent: :destroy
end
When I create a JobReport, tie it to a Job, and save it, postgres complains that the id doesn't exist in good_jobs, because it's trying to use the active_job_id:
irb(main):001:0> jr = JobReport.new; gj = GoodJob::Job.last
=>
#<GoodJob::Job:0x00007ff6950cda30
...
irb(main):002:0> jr.good_job = gj
=>
#<GoodJob::Job:0x00007ff6950cda30
...
irb(main):003:0> jr.save
/usr/local/bundle/gems/activerecord-7.0.4.1/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': PG::ForeignKeyViolation: ERROR: insert or update on table "job_reports" violates foreign key constraint "fk_rails_6135bfd69e" (ActiveRecord::InvalidForeignKey)
DETAIL: Key (good_job_id)=(fdc02e75-a06a-4727-b790-9a846f61ed7d) is not present in table "good_jobs".
/usr/local/bundle/gems/activerecord-7.0.4.1/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': ERROR: insert or update on table "job_reports" violates foreign key constraint "fk_rails_6135bfd69e" (PG::ForeignKeyViolation)
DETAIL: Key (good_job_id)=(fdc02e75-a06a-4727-b790-9a846f61ed7d) is not present in table "good_jobs".
irb(main):004:0> gj.id
=> "fdc02e75-a06a-4727-b790-9a846f61ed7d"
irb(main):005:0> gj.active_job_id
=> "fdc02e75-a06a-4727-b790-9a846f61ed7d"
irb(main):006:0> gj.attributes["id"]
=> "edc27b66-975d-4017-a09f-2d0cec332a0c"
As I mentioned before, if I give up on the ID column and switch to the active_job_id column, postgres says I can't use it as a foreign key b/c there's no uniqueness constraint. Sure, I could edit the GoodJob tables, but I'd prefer to use the drop-in form of the gem without tampering with it for upgrades and whatnot down the road.
Edit: I implemented Max's suggestion, but it's still trying to use the active_job_id column of the good_jobs table instead of the id column.
class JobReport < ApplicationRecord
belongs_to :good_job, class_name: 'GoodJob::Job', foreign_key: 'good_job_id', primary_key: 'id'
end
irb(main):010:0> jr = JobReport.new; gj = GoodJob::Job.last
=>
#<GoodJob::Job:0x00007f70ec430918
...
irb(main):011:0> jr.good_job = gj
=>
#<GoodJob::Job:0x00007f70ec430918
...
irb(main):012:0> jr.save
/usr/local/bundle/gems/activerecord-7.0.4.1/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': PG::ForeignKeyViolation: ERROR: insert or update on table "job_reports" violates foreign key constraint "fk_rails_6135bfd69e" (ActiveRecord::InvalidForeignKey)
DETAIL: Key (good_job_id)=(fdc02e75-a06a-4727-b790-9a846f61ed7d) is not present in table "good_jobs".
/usr/local/bundle/gems/activerecord-7.0.4.1/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': ERROR: insert or update on table "job_reports" violates foreign key constraint "fk_rails_6135bfd69e" (PG::ForeignKeyViolation)
DETAIL: Key (good_job_id)=(fdc02e75-a06a-4727-b790-9a846f61ed7d) is not present in table "good_jobs".
irb(main):013:0> gj.id
=> "fdc02e75-a06a-4727-b790-9a846f61ed7d"
irb(main):014:0> gj.active_job_id
=> "fdc02e75-a06a-4727-b790-9a846f61ed7d"
irb(main):015:0> gj.attributes['id']
=> "edc27b66-975d-4017-a09f-2d0cec332a0c"
Here's the schema of the two tables:
development=# \d good_jobs
Table "public.good_jobs"
Column | Type | Collation | Nullable | Default
---------------------+--------------------------------+-----------+----------+-------------------
id | uuid | | not null | gen_random_uuid()
queue_name | text | | |
priority | integer | | |
serialized_params | jsonb | | |
scheduled_at | timestamp(6) without time zone | | |
performed_at | timestamp(6) without time zone | | |
finished_at | timestamp(6) without time zone | | |
error | text | | |
created_at | timestamp(6) without time zone | | not null |
updated_at | timestamp(6) without time zone | | not null |
active_job_id | uuid | | |
concurrency_key | text | | |
cron_key | text | | |
retried_good_job_id | uuid | | |
cron_at | timestamp(6) without time zone | | |
batch_id | uuid | | |
batch_callback_id | uuid | | |
Indexes:
"good_jobs_pkey" PRIMARY KEY, btree (id)
"index_good_jobs_on_cron_key_and_cron_at" UNIQUE, btree (cron_key, cron_at)
"index_good_jobs_jobs_on_finished_at" btree (finished_at) WHERE retried_good_job_id IS NULL AND finished_at IS NOT NULL
"index_good_jobs_jobs_on_priority_created_at_when_unfinished" btree (priority DESC NULLS LAST, created_at) WHERE finished_at IS NULL
"index_good_jobs_on_active_job_id" btree (active_job_id)
"index_good_jobs_on_active_job_id_and_created_at" btree (active_job_id, created_at)
"index_good_jobs_on_batch_callback_id" btree (batch_callback_id) WHERE batch_callback_id IS NOT NULL
"index_good_jobs_on_batch_id" btree (batch_id) WHERE batch_id IS NOT NULL
"index_good_jobs_on_concurrency_key_when_unfinished" btree (concurrency_key) WHERE finished_at IS NULL
"index_good_jobs_on_cron_key_and_created_at" btree (cron_key, created_at)
"index_good_jobs_on_queue_name_and_scheduled_at" btree (queue_name, scheduled_at) WHERE finished_at IS NULL
"index_good_jobs_on_scheduled_at" btree (scheduled_at) WHERE finished_at IS NULL
Referenced by:
TABLE "job_reports" CONSTRAINT "fk_rails_6135bfd69e" FOREIGN KEY (good_job_id) REFERENCES good_jobs(id)
development=# \d job_reports
Table "public.job_reports"
Column | Type | Collation | Nullable | Default
-------------+--------------------------------+-----------+----------+-----------------------------------------
id | bigint | | not null | nextval('job_reports_id_seq'::regclass)
report | text | | |
good_job_id | uuid | | |
created_at | timestamp(6) without time zone | | not null |
updated_at | timestamp(6) without time zone | | not null |
Indexes:
"job_reports_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"fk_rails_6135bfd69e" FOREIGN KEY (good_job_id) REFERENCES good_jobs(id)
id
is whatever primary_key
is set to, even if they didn't override id method, they're also setting primary_key to active_job_id.
>> GoodJob::Job.last.id
=> "d781edac-1932-4d52-bfaa-61e4d80be5e8"
>> puts GoodJob::Job.instance_method(:id).source
def id
active_job_id
end
>> GoodJob::Job.remove_method(:id)
# now `id` method comes from `ActiveRecord`
# https://github.com/rails/rails/blob/v7.0.4.2/activerecord/lib/active_record/attribute_methods/primary_key.rb#L18
>> puts GoodJob::Job.instance_method(:id).source
def id
_read_attribute(@primary_key)
end
# `id` still returns `active_job_id`
>> GoodJob::Job.last.id
=> "d781edac-1932-4d52-bfaa-61e4d80be5e8"
# because
> GoodJob::Job.instance_variable_get("@primary_key")
=> "active_job_id"
ActiveRecord tries hard to use primary_key setting whenever id
is mentioned:
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/PrimaryKey.html
I have a few workarounds:
class JobReport < ApplicationRecord
belongs_to :good_job, class_name: "OkJob", foreign_key: :good_job_id
end
class OkJob < GoodJob::Job
self.primary_key = :id
def id
attributes["id"]
end
has_one :job_report, foreign_key: :good_job_id, dependent: :destroy
end
>> jr = JobReport.new; gj = OkJob.last; jr.good_job = gj; jr.save!
OkJob Load (2.3ms) SELECT "good_jobs".* FROM "good_jobs" WHERE "good_jobs"."retried_good_job_id" IS NULL ORDER BY "good_jobs"."id" DESC LIMIT $1 [["LIMIT", 1]]
TRANSACTION (0.4ms) BEGIN
JobReport Create (1.2ms) INSERT INTO "job_reports" ("report", "good_job_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["report", nil], ["good_job_id", "5301c9c7-2863-46cc-b8ea-7c959ed26474"], ["created_at", "2023-02-21 08:55:43.967530"], ["updated_at", "2023-02-21 08:55:43.967530"]]
TRANSACTION (2.8ms) COMMIT
=> true
>> jr.good_job_id == gj.attributes["id"]
=> true
This was my first attempt, it turned out to be a little awkward:
class JobReport < ApplicationRecord
# works when you're reading the association
belongs_to :good_job, class_name: "GoodJob::Job",
foreign_key: :good_job_id,
primary_key: :id,
optional: true
# but doesn't work when writing it,
# so this monstrosity takes care of it:
belongs_to :good_job_writer, class_name: "GoodJob::Job",
foreign_key: :good_job_id,
primary_key: :id_attribute,
optional: true
alias_method :good_job=, :good_job_writer=
end
module GoodJobJobDecorator
# doesn't work at all
# def self.prepended base
# base.has_one :job_report, primary_key: :id_attribute, foreign_key: :good_job_id, dependent: :destroy
# end ^
# i thought this was supposed to read from `id_attribute` method
# but it doesn't. oh, well ¯\_(ツ)_/¯
def job_report
JobReport.where(good_job_id: id_attribute).first
end
# `dependent: :destroy` is just a callback, you can add it manually
def id_attribute
attributes["id"]
end
end
GoodJob::Job.prepend(GoodJobJobDecorator)
>> jr = JobReport.new; gj = GoodJob::Job.last; jr.good_job = gj; jr.save!
GoodJob::Job Load (2.3ms) SELECT "good_jobs".* FROM "good_jobs" WHERE "good_jobs"."retried_good_job_id" IS NULL ORDER BY "good_jobs"."active_job_id" DESC LIMIT $1 [["LIMIT", 1]]
TRANSACTION (0.4ms) BEGIN
JobReport Create (1.3ms) INSERT INTO "job_reports" ("report", "good_job_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["report", nil], ["good_job_id", "5301c9c7-2863-46cc-b8ea-7c959ed26474"], ["created_at", "2023-02-21 09:12:59.543168"], ["updated_at", "2023-02-21 09:12:59.543168"]]
TRANSACTION (2.9ms) COMMIT
=> true
>> jr.good_job_id == gj.attributes["id"]
=> true
>> JobReport.last.good_job
=> #<GoodJob::Job:0x00007ff789df8c60>
You can certainly skip using associations, which is limiting, but makes a simpler set up:
class JobReport < ApplicationRecord
def good_job
GoodJob::Job.where(id: good_job_id).first
end
def good_job= job
self.good_job_id = job.attributes["id"]
end
end
>> jr = JobReport.new; gj = GoodJob::Job.last; jr.good_job = gj; jr.save!
GoodJob::Job Load (2.3ms) SELECT "good_jobs".* FROM "good_jobs" WHERE "good_jobs"."retried_good_job_id" IS NULL ORDER BY "good_jobs"."active_job_id" DESC LIMIT $1 [["LIMIT", 1]]
TRANSACTION (0.4ms) BEGIN
JobReport Create (1.2ms) INSERT INTO "job_reports" ("report", "good_job_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["report", nil], ["good_job_id", "5301c9c7-2863-46cc-b8ea-7c959ed26474"], ["created_at", "2023-02-21 09:32:52.589234"], ["updated_at", "2023-02-21 09:32:52.589234"]]
TRANSACTION (3.0ms) COMMIT
=> true
>> jr.good_job_id == gj.attributes["id"]
=> true