I am trying to implement a derived field in Rails 7. I have a Tag model. A tag has a prefix, a loop number, and an optional suffix. (Yes, I'm an instrument engineer :-) I keep these attributes separately in the database, because it is required to sort and search on them individually, but most of the time the tag is referenced and displayed as the combination of these fields. I defined an attribute accessor, and a setter function in private, to join the fields, and I set the after_find helper to trigger the setter. But they don't seem to work. In the console, Tag.first.full_tag
results in nil
, and nothing is displayed in views.
I would have thought this requirement is relatively common, but haven't found anything useful in my searches. Of course, I could probably persist the derived attribute into the database, but that would contravene the basic rule of "one source of information". I would greatly appreciate if someone can point out where I am going wrong.
# tag.rb:
class Tag < ApplicationRecord
belongs_to :project
belongs_to :discipline, foreign_key: :discipline, primary_key: :code
attr_reader :full_tag
after_find :set_full_tag
...
private
def set_full_tag
full_tag = prefix + "-" + loop.to_s.rjust(4, '0')
if !suffix.empty?
full_tag += "." + suffix
end
end
end
So, let's build an example here:
class Tag < ApplicationRecord
# attr_accessor for the derived full_tag
attr_accessor :full_tag
# Set the full_tag after finding the record
after_find :set_full_tag
private
def set_full_tag
# Set the full_tag on the instance using self.full_tag
self.full_tag = "#{prefix}-#{self.loop_number.to_s.rjust(4, '0')}"
# Add suffix if it's present
self.full_tag += ".#{suffix}" if suffix.present?
end
end
The main issue (as I see it) with the original code is that it does not correctly set the full_tag
value. To fix this, we first replace attr_reader
with attr_accessor
. Why? attr_reader
only lets you read the value, while attr_accessor
lets you both read and write it. As I understand it, you want to change the full_tag
value after finding the record.
Next, full_tag = ...
creates a temporary variable, which doesn't store the combined tag in the model. We can use self.full_tag = ...
, to tell Rails to use the setter method generated by attr_accessor
, so that the full_tag
value is properly updated and saved in the object.
I hope this points you in the right direction.
rails new tag_proj
cd tag_proj
rails generate model Tag prefix:string loop_number:integer suffix:string
rails db:migrate
echo "
class Tag < ApplicationRecord
attr_accessor :full_tag
after_find :set_full_tag
private
def set_full_tag
self.full_tag = \"#{prefix}-#{self.loop_number.to_s.rjust(4, '0')}\"
self.full_tag += \".#{suffix}\" if suffix.present?
end
end
" > app/models/tag.rb
echo "
Tag.create(prefix: 'XX', loop_number: 123, suffix: 'A')
Tag.create(prefix: 'YY', loop_number: 45, suffix: 'B')
Tag.create(prefix: 'ZZ', loop_number: 67, suffix: nil)
" >> db/seeds.rb
rails db:seed
echo "
# lib/tasks/tag_full_tag.rake
namespace :tag do
task full_tag: :environment do
puts Tag.first.full_tag
puts Tag.second.full_tag
puts Tag.third.full_tag
end
end
" > lib/tasks/tag_full_tag.rake
rake tag:full_tag
> rake tag:full_tag
XX-0123.A
YY-0045.B
ZZ-0067