Problem
You have a Tag model with separate columns: prefix, loop (a number), and optional suffix. You want a derived attribute full_tag (e.g., “PRE-0042.SFX”) for display and forms, without persisting it. Your attempt used attr_reader, an after_find callback, and a private setter that built a local variable, which resulted in nil in the console and views.

Why Your Current Approach Fails

1) attr_reader only exposes an instance variable (@full_tag). You never set @full_tag; inside set_full_tag you assign to a local variable full_tag, which is discarded.
2) You don’t need after_find at all for a computed value—just compute on demand.
3) Using loop as a column name is legal, but Kernel also defines loop. Always reference the attribute as self.loop (and self.loop= in writers) to avoid ambiguity.
4) suffix can be nil. Calling suffix.empty? will raise on nil. Use present?/blank? or to_s.

Solution A (Recommended): Compute full_tag on demand (no callbacks)

Define a plain Ruby method. It is nil‑safe and renders correctly in views and the console.

# app/models/tag.rb
class Tag < ApplicationRecord
  belongs_to :project
  belongs_to :discipline, foreign_key: :discipline, primary_key: :code

  # Virtual, read-only attribute
  def full_tag
   parts = []
   # prefix
   parts << prefix if prefix.present?
   # loop, left-padded to 4 (e.g., 42 => "0042")
   parts << self.loop.to_i.to_s.rjust(4, '0') if self.loop.present?
   base = parts.join('-')
   return base if suffix.blank?
   "#{base}.#{suffix}"
 end
end

Usage in views and console:

<%= @tag.full_tag %> 

# Rails console
Tag.first.full_tag

Solution B (Optional): Add a writer so forms can assign full_tag

If you want to accept a single input and split it into prefix, loop, and suffix, add a writer. Then permit :full_tag in strong parameters.

# app/models/tag.rb
class Tag < ApplicationRecord
  belongs_to :project
  belongs_to :discipline, foreign_key: :discipline, primary_key: :code

  def full_tag
   parts = []
   parts << prefix if prefix.present?
   parts << self.loop.to_i.to_s.rjust(4, '0') if self.loop.present?
   base = parts.join('-')
   suffix.present? ? "#{base}.#{suffix}" : base
  end

  # Virtual attribute writer, parses "PRE-0042.SFX" or "PRE-0042"
  def full_tag=(value)
   return if value.blank?
   head, suf = value.split('.', 2)
   pre, num = head.to_s.split('-', 2)

   self.prefix = pre
   # strip non-digits, store as integer (or string if your column is string)
   digits = num.to_s.gsub(/\D/, '')
   self.loop = digits.presence

   self.suffix = suf.presence
  end
end

Strong Parameters & Forms Controller

# app/controllers/tags_controller.rb
  def tag_params
   params.require(:tag).permit(:prefix, :loop, :suffix, :full_tag)
  end

Simple Form (single full_tag field):

<%= simple_form_for @tag do |f| %>
   <%= f.input :full_tag, label: "Tag" %>
   <%= f.button :submit %>
  <% end %>

Or show the computed value while still editing the parts:

<%= simple_form_for @tag do |f| %>
Full tag: <%= @tag.full_tag %>

 

<%= f.input :prefix %> <%= f.input :loop, label: “Loop number” %> <%= f.input :suffix %>

<%= f.button :submit %> <% end %>

Common Pitfalls (and Fixes)

• Local vs. instance variable: use @full_tag if you truly want to cache, but caching is unnecessary—prefer computing on demand.
• Avoid after_find: it’s extra work and can miss cases (e.g., new records, unsaved changes).
• Nil checks: use present?/blank? or to_s to guard against nil suffix/prefix.
• Column named loop: prefer self.loop and self.loop= in Ruby code; consider renaming to loop_number for clarity.
• Sorting & searching: keep sorting on the atomic columns (prefix, loop, suffix). If you need to search by the combined form, build an expression in SQL (e.g., using CONCAT/LTRIM/LPAD in your DB) or search on parts.

Need Help With Ruby On Rails Development?

Work with our skilled Ruby on Rails developers to accelerate your project and boost its performance.

Hire Ruby on Rails Developer

Support On Demand!

Related Q&A