{"title":"Models and Columns","description":"Defining models, columns, and data types in Grant ORM","section":"guides/models/grant","version":"v2","path":"guides/models/grant/basics","canonical_url":"https://amberframework.org/docs/v2/guides/models/grant/basics","markdown_url":"https://amberframework.org/docs/v2/guides/models/grant/basics.md","inherited":false,"content_markdown":"# Models and Columns\n\n> **Supported web path:** Amber CLI `2.0.6` includes Grant in every generated\n> web application and pins the reviewed V2 commit. Preserve that pin while\n> following this beta.\n\n## Where the examples go\n\nModel classes, columns, defaults, converters, and serialization declarations\nbelong under `src/models/`, one primary model per file. Register connections in\na direct file under `config/`, such as `config/database.cr`, so the V2 entry\npoint loads it through `require \"../config/*\"`. Usage expressions run from the\ncontroller, job, service, or spec that owns the operation. Blocks on this page\nuse those destinations unless a closer comment identifies another role.\n\nModels in Grant represent database tables and provide an object-oriented interface for data interaction.\n\n## Basic Model Definition\n\n```crystal\nclass User < Grant::Base\n  connection pg        # Database connection\n  table users         # Table name (optional, defaults to pluralized class name)\n\n  column id : Int64, primary: true\n  column email : String\n  column name : String\n  column active : Bool = true\n\n  timestamps          # Adds created_at and updated_at\nend\n```\n\n## Column Types\n\n### Primitive Types\n\n```crystal\nclass Product < Grant::Base\n  connection pg\n\n  # Integer types\n  column id : Int64, primary: true      # BIGINT\n  column quantity : Int32               # INTEGER\n  column position : Int16               # SMALLINT\n\n  # Floating point\n  column price : Float64                # DOUBLE PRECISION\n  column rating : Float32               # FLOAT\n\n  # String types\n  column name : String                  # VARCHAR/TEXT\n  column description : String?          # Nullable string\n\n  # Boolean\n  column active : Bool = true           # BOOLEAN\n\n  # Time/Date\n  column published_at : Time?           # TIMESTAMP\n\n  timestamps\nend\n```\n\n### Special Types\n\n```crystal\nclass AdvancedModel < Grant::Base\n  connection pg\n\n  # UUID (PostgreSQL, MySQL 8+)\n  column id : UUID, primary: true\n\n  # JSON (PostgreSQL JSONB, MySQL JSON)\n  column metadata : JSON::Any?\n  column settings : JSON::Any = JSON.parse(\"{}\")\n\n  # Arrays (PostgreSQL only)\n  column tags : Array(String)?\n  column scores : Array(Int32)?\n\n  # Binary data\n  column file_data : Bytes?\nend\n```\n\n## Column Options\n\n| Option | Description | Example |\n|--------|-------------|---------|\n| `primary: true` | Marks as primary key | `column id : Int64, primary: true` |\n| `auto: false` | Disables auto-increment | `column uuid : String, primary: true, auto: false` |\n| `converter:` | Custom type converter | `column data : JSON::Any, converter: Grant::Converters::Json` |\n| Default value | Sets default | `column active : Bool = true` |\n\n## Primary Keys\n\n### Standard Auto-increment\n\n```crystal\nclass User < Grant::Base\n  column id : Int64, primary: true\nend\n```\n\n### UUID Primary Key\n\n```crystal\nclass Document < Grant::Base\n  connection pg\n  column id : UUID, primary: true\n  column title : String\nend\n\ndoc = Document.new(title: \"Report\")\ndoc.save\ndoc.id # => \"550e8400-e29b-41d4-a716-446655440000\"\n```\n\n### Natural Key\n\n```crystal\nclass Country < Grant::Base\n  connection pg\n  column iso_code : String, primary: true, auto: false\n  column name : String\nend\n\nCountry.create!(iso_code: \"US\", name: \"United States\")\n```\n\n## Timestamps\n\n```crystal\nclass Post < Grant::Base\n  column id : Int64, primary: true\n  column title : String\n\n  timestamps  # Adds created_at and updated_at\nend\n\npost = Post.create!(title: \"Hello\")\npost.created_at  # => 2025-01-15 12:00:00 UTC\npost.updated_at  # => 2025-01-15 12:00:00 UTC\n\npost.update!(title: \"Hello World\")\npost.updated_at  # => 2025-01-15 12:05:00 UTC (updated)\n```\n\n## Default Values\n\n### Static Defaults\n\n```crystal\nclass Article < Grant::Base\n  column status : String = \"draft\"\n  column views : Int32 = 0\n  column featured : Bool = false\n  column tags : Array(String) = [] of String\nend\n```\n\n### Dynamic Defaults via Callbacks\n\n```crystal\nclass Token < Grant::Base\n  column value : String?\n  column expires_at : Time?\n\n  before_create :set_defaults\n\n  private def set_defaults\n    self.value ||= Random::Secure.hex(32)\n    self.expires_at ||= 24.hours.from_now\n  end\nend\n```\n\n## Multiple Database Connections\n\n### Registering Connections\n\n```crystal\n# config/database.cr\nGrant::Connections << Grant::Adapter::Pg.new(\n  name: \"primary\",\n  url: ENV[\"PRIMARY_DATABASE_URL\"]\n)\n\nGrant::Connections << Grant::Adapter::Mysql.new(\n  name: \"legacy\",\n  url: ENV[\"LEGACY_DATABASE_URL\"]\n)\n\nGrant::Connections << Grant::Adapter::Sqlite.new(\n  name: \"cache\",\n  url: \"sqlite3://./cache.db\"\n)\n```\n\n### Using Different Connections\n\n```crystal\nclass User < Grant::Base\n  connection primary\n  table users\nend\n\nclass LegacyCustomer < Grant::Base\n  connection legacy\n  table customers\nend\n\nclass CacheEntry < Grant::Base\n  connection cache\n  table cache_entries\nend\n```\n\n## Type Converters\n\n### Built-in Converters\n\n```crystal\n# Enum converter\nenum Status\n  Active\n  Inactive\n  Pending\nend\n\nclass Account < Grant::Base\n  column status : Status, converter: Grant::Converters::Enum(Status, String)\nend\n\n# JSON converter for custom types\nclass Settings\n  include JSON::Serializable\n  property theme : String = \"light\"\n  property notifications : Bool = true\nend\n\nclass User < Grant::Base\n  column preferences : Settings, converter: Grant::Converters::Json(Settings, String)\nend\n```\n\n### Custom Converters\n\n```crystal\nmodule Grant::Converters\n  class EncryptedString < Grant::Converters::Base(String, String)\n    def self.from_db(value : String) : String\n      decrypt(value)\n    end\n\n    def self.to_db(value : String) : String\n      encrypt(value)\n    end\n  end\nend\n\nclass SecureModel < Grant::Base\n  column secret : String, converter: Grant::Converters::EncryptedString\nend\n```\n\n## JSON Serialization\n\nGrant models include JSON::Serializable by default:\n\n```crystal\nuser = User.find(1)\njson = user.to_json\n# => {\"id\":1,\"name\":\"John\",\"email\":\"john@example.com\"}\n\n# Custom serialization\nclass User < Grant::Base\n  @[JSON::Field(key: \"user_name\")]\n  column name : String\n\n  @[JSON::Field(ignore: true)]\n  column password_hash : String?\nend\n```\n\n## Database-Specific Features\n\n### PostgreSQL\n\n```crystal\nclass PgModel < Grant::Base\n  connection pg\n\n  # Arrays\n  column tags : Array(String)\n\n  # JSONB\n  column metadata : JSON::Any\n\n  # Full-text search scope\n  scope :search, ->(query : String) {\n    where(\"to_tsvector('english', content) @@ plainto_tsquery('english', ?)\", [query])\n  }\nend\n```\n\n### MySQL\n\n```crystal\nclass MysqlModel < Grant::Base\n  connection mysql\n\n  # JSON column (MySQL 5.7+)\n  column settings : JSON::Any\n\n  # Full-text search\n  scope :search, ->(query : String) {\n    where(\"MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)\", [query])\n  }\nend\n```\n\n## Best Practices\n\n### 1. Choose Appropriate Types\n\n```crystal\n# Good: Use specific types\ncolumn price_cents : Int32      # Store money as integers\ncolumn email : String           # Validated elsewhere\ncolumn published : Bool         # Clear boolean\n\n# Avoid: Ambiguous types\ncolumn price : Float64          # Floating point money issues\ncolumn data : String            # Consider JSON::Any\n```\n\n### 2. Use Nullability Appropriately\n\n```crystal\n# Required fields (not nilable)\ncolumn email : String\ncolumn name : String\n\n# Optional fields (nilable)\ncolumn bio : String?\ncolumn deleted_at : Time?\n```\n\n### 3. Set Sensible Defaults\n\n```crystal\ncolumn status : String = \"pending\"\ncolumn retry_count : Int32 = 0\ncolumn active : Bool = true\n```"}