Granite ORM
repository·master·Indexed 43 days ago
https://github.com/amberframework/graniteAn 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.
What's inside Granite
- Granite is an Object-Relational Mapper (ORM) written in the Crystal language. It is designed to work with the Amber web framework and provides database abstraction capabilities for Crystal applications.
Configure One-to-Many relationships with `has_many` and `belongs_to`
masterOne-to-many relationships use
has_manyon the parent model andbelongs_toon 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 theclass_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 endHow to manually run callbacks for bulk imports
masterBecause
.importis 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
.savecall, 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.saveConfigure One-to-One relationships with `has_one` and `belongs_to`
masterIn Granite, one-to-one relationships are defined using the
has_onemacro on the parent model and thebelongs_tomacro 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, ifTeamhashas_one :coach, Granite expectsteam_idin thecoachestable. - Saving Associations: When assigning an association via the parent (e.g.,
team.coach = coach), you must call.saveon the child instance because the child holds the foreign key. - Limitations: One-to-one relationships do not currently support
throughassociations.
Customization
You can override the inferred class name or foreign key using
class_name:andforeign_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- Foreign Key Inference: By default, Granite infers the foreign key from the class name of the model using
Serialize models to JSON and YAML
masterGranite models implement
JSON::SerializableandYAML::Serializableby default. You can convert model instances to/from JSON or YAML using the#to_json/#to_yamland.from_json/.from_yamlmethods.# Example usage (conceptual) json_data = my_model.to_json new_model = MyModel.from_json(json_data)Define primary keys in Granite models
masterEvery model requires a primary key. You define it using the
columnmacro with theprimary: trueoption.Key constraints:
- Composite primary keys are not currently supported.
- You can use
belongs_toassociations as primary keys by addingprimary: trueto the association. - By default, primary keys are assumed to be auto-incrementing. For natural keys, set
auto: false. - For databases using UUIDs, use the
UUIDtype; 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 endSet up a standalone Micrate CLI executable
masterTo run migration commands from the command line, create an executable script (e.g.,
bin/micrate). You must requiremicrateand the specific database driver you are using (e.g.,pgfor PostgreSQL). You must also configureMicrate::DB.connection_urlusing your database connection string, typically sourced from theDATABASE_URLenvironment 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.runSet up a local testing environment for Granite
masterTo run Granite tests locally without Docker, follow these steps:
- Install dependencies: Run
shards install. - Configure Environment: Update your
.envfile with the appropriate environment variables or manually create the required databases. - Database Setup: Create the user and database required for Granite. See the specific SQL commands for PostgreSQL or MySQL below.
- Export Environment: Run
source ./export.shorsource .env. - 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- Install dependencies: Run
Register and use multiple database connections
masterYou can register multiple database connections globally using
Granite::Connections << adapter_object. Once registered, models can specify which connection to use by calling theconnectionmethod 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 endInstall Granite and database drivers
masterTo use Granite, add it to your project's
shard.ymldependencies 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, andSqlite.dependencies: granite: github: amberframework/granite # Pick your database mysql: github: crystal-lang/crystal-mysql sqlite3: github: crystal-lang/crystal-sqlite3 pg: github: will/crystal-pgGenerate documentation including Granite symbols
masterBy default,
crystal docsexcludes Granite's methods, constants, and properties. To include them in your generated documentation, use thegranite_docsflag:crystal docs -D granite_docsImplement Many-to-Many relationships using join models
masterGranite 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) thatbelongs_toboth entities.Setup Steps
- Create the join model (e.g.,
Participant) withbelongs_toassociations for both sides. - Add
has_manyassociations to the primary models pointing to the join model. - 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- Create the join model (e.g.,