Models and Columns
Preview ecosystem guide: Grant is not part of the Amber 2.0.0-beta.2 core web-app release gate. Its package version, API, and platform support may change independently. Do not add a personal fork as a default dependency.
Models in Grant represent database tables and provide an object-oriented interface for data interaction.
Basic Model Definition
class User < Grant::Base
connection pg # Database connection
table users # Table name (optional, defaults to pluralized class name)
column id : Int64, primary: true
column email : String
column name : String
column active : Bool = true
timestamps # Adds created_at and updated_at
end
Column Types
Primitive Types
class Product < Grant::Base
connection pg
# Integer types
column id : Int64, primary: true # BIGINT
column quantity : Int32 # INTEGER
column position : Int16 # SMALLINT
# Floating point
column price : Float64 # DOUBLE PRECISION
column rating : Float32 # FLOAT
# String types
column name : String # VARCHAR/TEXT
column description : String? # Nullable string
# Boolean
column active : Bool = true # BOOLEAN
# Time/Date
column published_at : Time? # TIMESTAMP
timestamps
end
Special Types
class AdvancedModel < Grant::Base
connection pg
# UUID (PostgreSQL, MySQL 8+)
column id : UUID, primary: true
# JSON (PostgreSQL JSONB, MySQL JSON)
column metadata : JSON::Any?
column settings : JSON::Any = JSON.parse("{}")
# Arrays (PostgreSQL only)
column tags : Array(String)?
column scores : Array(Int32)?
# Binary data
column file_data : Bytes?
end
Column Options
| Option | Description | Example |
|--------|-------------|---------|
| primary: true | Marks as primary key | column id : Int64, primary: true |
| auto: false | Disables auto-increment | column uuid : String, primary: true, auto: false |
| converter: | Custom type converter | column data : JSON::Any, converter: Grant::Converters::Json |
| Default value | Sets default | column active : Bool = true |
Primary Keys
Standard Auto-increment
class User < Grant::Base
column id : Int64, primary: true
end
UUID Primary Key
class Document < Grant::Base
connection pg
column id : UUID, primary: true
column title : String
end
doc = Document.new(title: "Report")
doc.save
doc.id # => "550e8400-e29b-41d4-a716-446655440000"
Natural Key
class Country < Grant::Base
connection pg
column iso_code : String, primary: true, auto: false
column name : String
end
Country.create!(iso_code: "US", name: "United States")
Timestamps
class Post < Grant::Base
column id : Int64, primary: true
column title : String
timestamps # Adds created_at and updated_at
end
post = Post.create!(title: "Hello")
post.created_at # => 2025-01-15 12:00:00 UTC
post.updated_at # => 2025-01-15 12:00:00 UTC
post.update!(title: "Hello World")
post.updated_at # => 2025-01-15 12:05:00 UTC (updated)
Default Values
Static Defaults
class Article < Grant::Base
column status : String = "draft"
column views : Int32 = 0
column featured : Bool = false
column tags : Array(String) = [] of String
end
Dynamic Defaults via Callbacks
class Token < Grant::Base
column value : String?
column expires_at : Time?
before_create :set_defaults
private def set_defaults
self.value ||= Random::Secure.hex(32)
self.expires_at ||= 24.hours.from_now
end
end
Multiple Database Connections
Registering Connections
# config/database.cr
Grant::Connections << Grant::Adapter::Pg.new(
name: "primary",
url: ENV["PRIMARY_DATABASE_URL"]
)
Grant::Connections << Grant::Adapter::Mysql.new(
name: "legacy",
url: ENV["LEGACY_DATABASE_URL"]
)
Grant::Connections << Grant::Adapter::Sqlite.new(
name: "cache",
url: "sqlite3://./cache.db"
)
Using Different Connections
class User < Grant::Base
connection primary
table users
end
class LegacyCustomer < Grant::Base
connection legacy
table customers
end
class CacheEntry < Grant::Base
connection cache
table cache_entries
end
Type Converters
Built-in Converters
# Enum converter
enum Status
Active
Inactive
Pending
end
class Account < Grant::Base
column status : Status, converter: Grant::Converters::Enum(Status, String)
end
# JSON converter for custom types
class Settings
include JSON::Serializable
property theme : String = "light"
property notifications : Bool = true
end
class User < Grant::Base
column preferences : Settings, converter: Grant::Converters::Json(Settings, String)
end
Custom Converters
module Grant::Converters
class EncryptedString < Grant::Converters::Base(String, String)
def self.from_db(value : String) : String
decrypt(value)
end
def self.to_db(value : String) : String
encrypt(value)
end
end
end
class SecureModel < Grant::Base
column secret : String, converter: Grant::Converters::EncryptedString
end
JSON Serialization
Grant models include JSON::Serializable by default:
user = User.find(1)
json = user.to_json
# => {"id":1,"name":"John","email":"[email protected]"}
# Custom serialization
class User < Grant::Base
@[JSON::Field(key: "user_name")]
column name : String
@[JSON::Field(ignore: true)]
column password_hash : String?
end
Database-Specific Features
PostgreSQL
class PgModel < Grant::Base
connection pg
# Arrays
column tags : Array(String)
# JSONB
column metadata : JSON::Any
# Full-text search scope
scope :search, ->(query : String) {
where("to_tsvector('english', content) @@ plainto_tsquery('english', ?)", [query])
}
end
MySQL
class MysqlModel < Grant::Base
connection mysql
# JSON column (MySQL 5.7+)
column settings : JSON::Any
# Full-text search
scope :search, ->(query : String) {
where("MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)", [query])
}
end
Best Practices
1. Choose Appropriate Types
# Good: Use specific types
column price_cents : Int32 # Store money as integers
column email : String # Validated elsewhere
column published : Bool # Clear boolean
# Avoid: Ambiguous types
column price : Float64 # Floating point money issues
column data : String # Consider JSON::Any
2. Use Nullability Appropriately
# Required fields (not nilable)
column email : String
column name : String
# Optional fields (nilable)
column bio : String?
column deleted_at : Time?
3. Set Sensible Defaults
column status : String = "pending"
column retry_count : Int32 = 0
column active : Bool = true