class AddMetadataToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :metadata, :jsonb, default: {}, null: false
add_index :users, :metadata, using: :gin
end
end
class User < ApplicationRecord
store_accessor :metadata, :theme, :timezone, :email_notifications, :beta_features
# Query users with specific metadata values
scope :with_beta_features, -> { where("metadata @> ?", { beta_features: true }.to_json) }
scope :in_timezone, ->(tz) { where("metadata->>'timezone' = ?", tz) }
# Default values
after_initialize do
self.metadata ||= {}
self.theme ||= 'light'
self.timezone ||= 'UTC'
self.email_notifications = true if email_notifications.nil?
end
end
module Api
module V1
class UserPreferencesController < BaseController
before_action :authenticate_user!
def update
current_user.update!(preference_params)
render json: { metadata: current_user.metadata }, status: :ok
end
private
def preference_params
params.require(:preferences).permit(:theme, :timezone, :email_notifications, beta_features: [])
end
end
end
end
PostgreSQL's jsonb columns provide schema flexibility for semi-structured data without sacrificing query performance. I use JSON columns for user preferences, feature flags, or metadata that varies by record type. Unlike traditional EAV patterns, jsonb supports indexing and efficient queries via GIN indexes and operators like @> (contains) or -> (extract). The store_accessor method creates virtual attributes that feel like regular columns while being stored in the JSON field. This approach balances structure and flexibility—core attributes remain as columns for performance and integrity, while optional or variable attributes live in JSON. The downside is reduced type safety and schema enforcement compared to dedicated columns.
Related snips
-- Simple function
CREATE OR REPLACE FUNCTION get_full_name(
first_name VARCHAR,
last_name VARCHAR
)
RETURNS VARCHAR AS $$
Stored procedures and functions in PostgreSQL
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.