Granite ORM

repository·master·Indexed 43 days ago

https://github.com/amberframework/granite

An Object-Relational Mapper (ORM) for the Crystal programming language, designed for database abstraction and integration within the Amber web framework ecosystem. It supports Mysql, Pg, and Sqlite adapters and provides features for model definition, lifecycle callbacks, CRUD operations, and bulk imports.

Tokens
10.5K
Snippets
45
Records
51
Agent score
54%

What's inside Granite

  1. Configure One-to-Many relationships with `has_many` and `belongs_to`

    master

    One-to-many relationships use has_many on the parent model and belongs_to on the child model. This creates a Rails-like mapping between objects.

    Implementation Details

    • has_many: Returns an array of associated objects. If you use a pluralized name (e.g., has_many posts :Post), you must provide the class name. Alternatively, use the class_name: option.
    • belongs_to: Adds an instance method to access the parent and an assignment method (e.g., post.user = user) to set the relationship.
    • Foreign Keys: The child table must contain the foreign key (e.g., user_id) and an index for performance.
    class User < Granite::Base
      has_many :posts, class_name: Post, foreign_key: :custom_id
    
      column id : Int64, primary: true
      column name : String
    end
    
    class Post < Granite::Base
      belongs_to :user, foreign_key: :user_uuid
    
      column id : Int64, primary: true
      column title : String
    end
  2. How to manually run callbacks for bulk imports

    master

    Because .import is a class-level method, it bypasses the standard instance lifecycle hooks. If your model requires a callback to set a primary key (e.g., a UUID), you must manually invoke the callback on each instance before importing.

    Warning: Do not manually run callbacks before a standard .save call, as this will cause the callbacks to execute twice.

    # Example: Manually triggering before_create for a collection of items
    items = [
      Item.new(item_name: "item1"),
      Item.new(item_name: "item2"),
    ]
    
    # 1. Manually run the callback to ensure item_id is generated
    items.each(&.before_create)
    
    # 2. Perform the bulk import
    Item.import(items)
    
    # For a single record:
    item = Item.new(item_name: "item5")
    item.before_create
    item.save
  3. Configure One-to-One relationships with `has_one` and `belongs_to`

    master

    In Granite, one-to-one relationships are defined using the has_one macro on the parent model and the belongs_to macro on the child model. The child model must contain the foreign key.

    Key Behaviors

    • Foreign Key Inference: By default, Granite infers the foreign key from the class name of the model using has_one. For example, if Team has has_one :coach, Granite expects team_id in the coaches table.
    • Saving Associations: When assigning an association via the parent (e.g., team.coach = coach), you must call .save on the child instance because the child holds the foreign key.
    • Limitations: One-to-one relationships do not currently support through associations.

    Customization

    You can override the inferred class name or foreign key using class_name: and foreign_key: options.

    class Team < Granite::Base
      has_one :coach, class_name: Coach, foreign_key: :custom_id
    
      column id : Int64, primary: true
      column name : String
    end
    
    class Coach < Granite::Base
      belongs_to :team, foreign_key: :team_uuid
    
      column id : Int64, primary: true
    end
  4. Serialize models to JSON and YAML

    master

    Granite models implement JSON::Serializable and YAML::Serializable by default. You can convert model instances to/from JSON or YAML using the #to_json/#to_yaml and .from_json/.from_yaml methods.

    # Example usage (conceptual)
    json_data = my_model.to_json
    new_model = MyModel.from_json(json_data)
  5. Define primary keys in Granite models

    master

    Every model requires a primary key. You define it using the column macro with the primary: true option.

    Key constraints:

    • Composite primary keys are not currently supported.
    • You can use belongs_to associations as primary keys by adding primary: true to the association.
    • By default, primary keys are assumed to be auto-incrementing. For natural keys, set auto: false.
    • For databases using UUIDs, use the UUID type; Granite will automatically generate a secure RFC4122 V4 UUID when the model is saved.
    # Standard Int64 primary key
    class Site < Granite::Base
      column id : Int64, primary: true
    end
    
    # Natural key (no auto-increment)
    class Site < Granite::Base
      column custom_id : Int32, primary: true, auto: false
    end
    
    # UUID primary key
    class Book < Granite::Base
      column isbn : UUID, primary: true
    end
    
    # Association as primary key
    class ChatSettings < Granite::Base
      belongs_to chat : Chat, primary: true
    end
  6. Set up a standalone Micrate CLI executable

    master

    To run migration commands from the command line, create an executable script (e.g., bin/micrate). You must require micrate and the specific database driver you are using (e.g., pg for PostgreSQL). You must also configure Micrate::DB.connection_url using your database connection string, typically sourced from the DATABASE_URL environment variable.

    #! /usr/bin/env crystal
    #
    # To build a standalone command line client, require the
    # driver you wish to use and use `Micrate::Cli`.
    #
    
    require "micrate"
    require "pg"
    
    Micrate::DB.connection_url = ENV["DATABASE_URL"]
    Micrate::Cli.run
  7. Set up a local testing environment for Granite

    master

    To run Granite tests locally without Docker, follow these steps:

    1. Install dependencies: Run shards install.
    2. Configure Environment: Update your .env file with the appropriate environment variables or manually create the required databases.
    3. Database Setup: Create the user and database required for Granite. See the specific SQL commands for PostgreSQL or MySQL below.
    4. Export Environment: Run source ./export.sh or source .env.
    5. Run Tests: Execute crystal spec.

    Database Configuration SQL

    PostgreSQL

    CREATE USER granite WITH PASSWORD 'password';
    CREATE DATABASE granite_db;
    GRANT ALL PRIVILEGES ON DATABASE granite_db TO granite;

    MySQL

    CREATE USER 'granite'@'localhost' IDENTIFIED BY 'password';
    CREATE DATABASE granite_db;
    GRANT ALL PRIVILEGES ON granite_db.* TO 'granite'@'localhost' WITH GRANT OPTION;
    shards install
    # ... setup databases ...
    source .env
    crystal spec
  8. Register and use multiple database connections

    master

    You can register multiple database connections globally using Granite::Connections << adapter_object. Once registered, models can specify which connection to use by calling the connection method with the name assigned during registration.

    Note that you are responsible for supplying the connection URL (e.g., via environment variables) when initializing the adapter.

    Granite::Connections << Granite::Adapter::Mysql.new(name: "legacy_db", url: "LEGACY_DB_URL")
    Granite::Connections << Granite::Adapter::Pg.new(name: "new_db", url: "NEW_DB_URL")
    
    class Foo < Granite::Base
      connection legacy_db
    end
    
    class Bar < Granite::Base
      connection new_db
    end
  9. Install Granite and database drivers

    master

    To use Granite, add it to your project's shard.yml dependencies along with the driver for your chosen database. Granite is designed to work with the Amber framework but is compatible with Kemal or any other Crystal framework.

    Supported database adapters include Mysql, Pg, and Sqlite.

    dependencies:
      granite:
        github: amberframework/granite
    
      # Pick your database
      mysql:
        github: crystal-lang/crystal-mysql
    
      sqlite3:
        github: crystal-lang/crystal-sqlite3
    
      pg:
        github: will/crystal-pg
  10. Implement Many-to-Many relationships using join models

    master

    Granite recommends explicitly creating a model for join tables rather than using hidden tables. To implement a many-to-many relationship (e.g., Users and Rooms), create a third model (e.g., Participant) that belongs_to both entities.

    Setup Steps

    1. Create the join model (e.g., Participant) with belongs_to associations for both sides.
    2. Add has_many associations to the primary models pointing to the join model.
    3. Ensure the join table in the database contains foreign keys for both related entities and appropriate indexes.
    class User < Granite::Base
      has_many :participants, class_name: Participant
    end
    
    class Participant < Granite::Base
      table participants
      belongs_to :user
      belongs_to :room
    end
    
    class Room < Granite::Base
      table rooms
      has_many :participants, class_name: Participant
    end