Overview of Plutonium resource models - structure, setup, and best practices
Always use generators to create models - never create model files manually:
rails g pu:res:scaffold Post title:string content:text --dest=main_app
See plutonium-create-resource for full field type syntax and generator options.
A model becomes a Plutonium resource by including Plutonium::Resource::Record. This provides enhanced ActiveRecord functionality for routing, labeling, field introspection, associations, and monetary handling.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
include Plutonium::Resource::Record
primary_abstract_class
end
# app/models/resource_record.rb (optional abstract class)
class ResourceRecord < ApplicationRecord
self.abstract_class = true
end
# app/models/property.rb
class Property < ResourceRecord
# Now has access to all Plutonium features
end
Plutonium::Resource::Record includes six modules:
| Module | Purpose |
|---|---|
HasCents |
Monetary value handling (cents → decimal) |
Routes |
URL parameters, path customization |
Labeling |
Human-readable to_label method |
FieldNames |
Field introspection and categorization |
Associations |
SGID support for secure serialization |
AssociatedWith |
Entity scoping for multi-tenant apps |
Follow the template structure (comment markers indicate where to add code):
class Property < ResourceRecord
# add concerns above.
TYPES = {apartment: "Apartment", house: "House"}.freeze
# add constants above.
enum :state, archived: 0, active: 1
enum :property_class, residential: 0, commercial: 1
# add enums above.
has_cents :market_value_cents
# add model configurations above.
belongs_to :company
# add belongs_to associations above.
has_one :address
# add has_one associations above.
has_many :units
has_many :amenities, class_name: "PropertyAmenity"
# add has_many associations above.
has_one_attached :photo
has_many_attached :documents
# add attachments above.
scope :active, -> { where(state: :active) }
scope :by_company, ->(company) { where(company: company) }
# add scopes above.
validates :name, presence: true
validates :property_code, presence: true, uniqueness: {scope: :company_id}
# add validations above.
before_validation :generate_code, on: :create
# add callbacks above.
delegate :name, to: :company, prefix: true
# add delegations above.
has_rich_text :description
# add misc attribute macros above.
def full_address
address&.to_s
end
# add methods above. add private methods below.
private
def generate_code
self.property_code ||= SecureRandom.hex(4).upcase
end
end
include statementsTYPES = {...}.freeze, etc.enum :state, ...has_centshas_one_attached, has_many_attachedhas_rich_text, has_secure_token, has_secure_passwordStore monetary values as integers (cents) while exposing decimal interfaces.
class Product < ResourceRecord
has_cents :price_cents # Creates price getter/setter
has_cents :cost_cents, name: :wholesale # Custom accessor name
has_cents :tax_cents, rate: 1000 # 3 decimal places
has_cents :quantity_cents, rate: 1 # Whole numbers only
end
product = Product.new
product.price = 19.99
product.price_cents # => 1999
product.price # => 19.99
# Truncates (doesn't round)
product.price = 10.999
product.price_cents # => 1099
has_cents :field_cents,
name: :custom_name, # Accessor name (default: field without _cents)
rate: 100, # Conversion rate (default: 100)
suffix: "amount" # Suffix for generated name (default: "amount")
class Product < ResourceRecord
has_cents :price_cents
# Validate the cents field
validates :price_cents, numericality: {greater_than: 0}
end
product = Product.new(price: -10)
product.valid? # => false
product.errors[:price_cents] # => ["must be greater than 0"]
product.errors[:price] # => ["is invalid"] (propagated)
Product.has_cents_attributes
# => {price_cents: {name: :price, rate: 100}, ...}
Product.has_cents_attribute?(:price_cents) # => true
All associations get Signed Global ID (SGID) methods for secure serialization.
class Post < ResourceRecord
belongs_to :user
has_one :featured_image
end
post = Post.first
# Get SGID
post.user_sgid # => "BAh7CEkiCG..."
post.featured_image_sgid # => "BAh7CEkiCG..."
# Set by SGID (finds and assigns)
post.user_sgid = "BAh7CEkiCG..."
post.featured_image_sgid = "BAh7CEkiCG..."
class User < ResourceRecord
has_many :posts
has_and_belongs_to_many :roles
end
user = User.first
# Get SGIDs
user.post_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
user.role_sgids # => ["BAh7CEkiCG...", "BAh7CEkiCG..."]
# Bulk assignment
user.post_sgids = ["BAh7CEkiCG...", ...]
# Individual manipulation
user.add_post_sgid("BAh7CEkiCG...") # Add to collection
user.remove_post_sgid("BAh7CEkiCG...") # Remove from collection
Query records associated with another record. Essential for multi-tenant apps.
class Comment < ResourceRecord
belongs_to :post
end
# Find comments for a post
Comment.associated_with(post)
# => Comment.where(post: post)
Works with:
belongs_to - Uses WHERE clause (most efficient)has_one - Uses JOIN + WHEREhas_many - Uses JOIN + WHERE# Direct association (preferred)
Comment.associated_with(post) # WHERE post_id = ?
# Reverse association (less efficient, logs warning)
Post.associated_with(comment) # JOIN comments WHERE comments.id = ?
For optimal performance, define custom scopes:
class Comment < ResourceRecord
# Custom scope naming: associated_with_{model_name}
scope :associated_with_user, ->(user) do
joins(:post).where(posts: {user_id: user.id})
end
end
# Automatically uses custom scope
Comment.associated_with(user)
# When no association exists
UnrelatedModel.associated_with(user)
# Raises: Could not resolve the association between 'UnrelatedModel' and 'User'
#
# Define:
# 1. the associations between the models
# 2. a named scope on UnrelatedModel e.g.
#
# scope :associated_with_user, ->(user) { do_something_here }
user = User.find(1)
user.to_param # => "1"
Use a stable, unique field instead of ID:
class User < ResourceRecord
private
def path_parameter(param_name)
:username # Must be unique
end
end
user = User.create(username: "john_doe")
user.to_param # => "john_doe"
# URLs: /users/john_doe
Include ID prefix for uniqueness with human-readable suffix:
class Article < ResourceRecord
private
def dynamic_path_parameter(param_name)
:title
end
end
article = Article.create(id: 1, title: "My Great Article")
article.to_param # => "1-my-great-article"
# URLs: /articles/1-my-great-article
User.from_path_param("john_doe")
Article.from_path_param("1-my-great-article") # Extracts ID
The to_label method provides human-readable record labels:
# Automatic - checks :name, then :title, then fallback
user = User.new(name: "John Doe")
user.to_label # => "John Doe"
user = User.create(id: 1)
user.to_label # => "User #1"
# Custom override
class Product < ResourceRecord
def to_label
"#{name} (#{sku})"
end
end
Access field information programmatically:
# All resource fields
User.resource_field_names
# => [:id, :name, :email, :company, :avatar, ...]
# By category
User.content_column_field_names # Database columns
User.belongs_to_association_field_names # belongs_to associations
User.has_one_association_field_names # has_one associations
User.has_many_association_field_names # has_many associations
User.has_one_attached_field_names # Active Storage single
User.has_many_attached_field_names # Active Storage multiple
class Property < ResourceRecord
enum :state, archived: 0, active: 1
scope :active, -> { where(state: :active) }
scope :archived, -> { where(state: :archived) }
end
class Property < ResourceRecord
belongs_to :company
# Compound uniqueness for multi-tenant
validates :property_code, uniqueness: {scope: :company_id}
# Custom scope for entity scoping
scope :associated_with_company, ->(company) { where(company: company) }
end
# Efficient: Direct belongs_to
Comment.associated_with(post) # Simple WHERE
# Less efficient: Reverse has_many (logs warning)
Post.associated_with(comment) # JOIN required
# Optimal: Custom scope when direct isn't possible
scope :associated_with_user, ->(user) { where(user_id: user.id) }
# SGID: Batch assignment over individual adds
user.post_sgids = sgid_array # Single operation
enum :state, archived: 0, active: 1 instead of soft-deleteplutonium-create-resource - Scaffold generator for new resourcesplutonium-definition - Definition overview, fields, inputs, displays