Crystal compiles to a single fast binary, has a Ruby-like syntax, and — less happily — gives you exactly three serious ways to talk to a database. The signs are contradictory. crystal-db, the official driver abstraction, sits at 312 stars with commits as recent as 2026-08-20. Jennifer, the richest ORM in the ecosystem at 424 stars, has not been pushed since 2025-01-25 and its README carries a blunt warning: MySQL 8.0.36 and above is not supported. Granite, the ORM built for the Amber framework at 308 stars and an 2026-08-05 push, has a README headline that simply says “Looking for maintainers.”
So the choice is not “which is best” — it is “which failure mode can you live with.” Here is what each layer actually gives you, based on the current state of their repositories.
TL;DR: Quick Verdict
Use crystal-db directly if your app has a handful of queries per endpoint — you get the official shard, connection pooling, and no ORM abstraction tax. Use Jennifer if you need real ORM features (associations, validations, migration DSL, CTEs, JSON operators) and your database is MySQL < 8.0.36, PostgreSQL or SQLite. Use Granite if you are already building on Amber or Kemal and want model classes with a plain SQL fallback — but read the maintainership notice before you commit two years of code to it.
Feature Comparison at a Glance
| Dimension | crystal-db | Jennifer | Granite |
|---|---|---|---|
| GitHub stars | 312 | 424 | 308 |
| Last repository push | 2026-08-20 | 2025-01-25 | 2026-08-05 |
| Abstraction level | Driver API (no models) | Full ActiveRecord-style ORM | Model layer + query builder |
| Config file | shard.yml + connection URI | shard.yml + database.yml / DATABASE_URL | shard.yml + registered connection |
| Migrations | None (bring your own) | Built in, via sam.cr | Delegated to micrate |
| Query style | SQL strings, parameterised | Chainable DSL + SQL | Query builder + raw SQL via #all |
| Associations | Manual joins | belongs_to, has_many, has_one, HABTM, polymorphic | belongs_to, has_many, has_one |
| Validations | None | Built in, extendable | Built in |
| License | MIT | MIT | MIT |
| Maintenance signal | Officially maintained by the Crystal team | Slow; MySQL 8.0.36+ unsupported | Actively seeking maintainers |
| Best fit | Small services, analytics jobs, maximum control | Feature-complete apps on older MySQL/PG/SQLite | Amber or Kemal projects, SQL-first teams |
Decision Matrix: Pick by Use Case
| Your situation | Recommended layer | Why |
|---|---|---|
| A service with 10 endpoints and simple queries | crystal-db | Pooling and prepared statements are enough; an ORM adds mapping code you will not use |
| A CRUD app with many relations | Jennifer | Associations, validations and eager loading are exactly what you would otherwise hand-roll |
| Already on Amber (or Kemal) | Granite | Framework integration, model conventions and micrate migrations are ready to use |
| Reporting or batch jobs with heavy SQL | crystal-db | You want the SQL verbatim plus pooled connections, not an AST |
| MySQL 8.0.36 or newer | crystal-db | Jennifer explicitly does not support it; the driver layer does |
| Teams that prefer explicit schema files | Granite + micrate | Migrations are plain SQL files with an Up/Down marker |
| Apps that must survive upstream abandonment | crystal-db | It is the officially maintained building block the other two sit on |
| You need CTEs and JSON operators in the DSL | Jennifer | CTE support and JSON operators are first-class in its DSL |
crystal-db: The Layer Everything Else Sits On
crystal-db is not an ORM and does not pretend to be. It defines a common API, and each database is a separate driver. The installation instructions from the README are deliberately split by intent — a library that should work with any driver depends on the abstraction; an application depends on the driver:
| |
| |
The available drivers, taken from the project’s own README, are SQLite (crystal-lang/crystal-sqlite3), MySQL (crystal-lang/crystal-mysql), PostgreSQL (will/crystal-pg), ODBC (naqvis/crystal-odbc), Cassandra (kaukas/crystal-cassandra), DuckDB (amauryt/crystal-duckdb), Microsoft SQL Server (wonderix/crystal-tds) and Mimer SQL (majorproblem/crystal-mimer). Of those, the PostgreSQL driver is the most actively developed at 481 stars with a push on 2026-09-07, and SQLite sits at 161 stars with an 2026-08-09 push.
Usage is plain SQL with real parameter binding. The example below is the README’s, with the PostgreSQL note it makes in the comment:
| |
Two things in that snippet matter at scale. DB.open returns a pooled connection object, so you are not opening a socket per query; and db.scalar avoids materialising a result set when you only want one value. The SQL is not interpreted — ? versus $1 placeholders are the driver’s business, as the comment states. What you do not get is any of the ORM conveniences: no model classes, no migrations, no validations. If you find yourself writing a third ResultSet-to-struct mapper, that is the signal to move up a layer. And if pooling becomes the bottleneck, the operational patterns in our connection pool monitoring guide for PgBouncer, Pgpool and Odyssey apply to Crystal services exactly as they do to any other PostgreSQL client.
Jennifer: Full ORM, With One Sharp Caveat
Jennifer is the most complete ORM Crystal has. Its README states the feature set plainly: flexible model schema definition, relationships including polymorphic ones, extendable validations, query scopes, callbacks, database view support and SQL translations. It also ships a migration system driven by a small CLI called Sam.
Start with the shard entry, exactly as documented:
| |
Migrations are generated and then written in Crystal. This is the README’s own example:
| |
Models declare a mapping block and relationships:
| |
The query DSL is where Jennifer earns its keep — eager loading, aggregates and null ordering are all expressible without dropping to SQL:
| |
#explain on a query object is the underrated feature here: you can assert on plans in specs instead of guessing why a page slowed down. Logging integrates with Crystal’s standard Log module rather than a private framework:
| |
Now the caveats, which are the reason this layer needs a decision rather than a default. The README states that MySQL 8.0.36 and above is not supported at the moment — that rules Jennifer out for many current-managed MySQL deployments. The last repository push was 2025-01-25, so upstream velocity is low. And its tooling assumes you adopt Sam as a command runner. None of these are fatal for a PostgreSQL application with a stable schema, but all three should be part of your risk assessment.
Granite: Models for Amber and Kemal
Granite is what most Amber applications use, and its README points out it works with Kemal or anything else too.

Dependencies are declared per driver:
| |
A connection is registered before the models are loaded, and a model looks like this:
| |
Queries use a builder that supports operators and raw clauses side by side, which is Granite’s main ergonomic advantage over hand-written SQL:
| |
Supported operators are :eq, :gteq, :lteq, :neq, :gt, :lt, :nlt, :ngt, :ltgt, :in, :nin, :like and :nlike. Migrations are not built in; the docs delegate them to micrate, with a tiny CLI shim and plain SQL migration files:
| |
| |
The trade-off is the same as any SQL-file workflow: nothing infers your schema from your models, so the two can drift. Granite’s own repository even ships a Docker Compose setup for running its specs against a real database, which is a good starting point for a reproducible development stack:
| |
Rename spec to your application service, keep the pg service, and you have the same topology the maintainers test against. That is the strongest argument for Granite: a real, versioned Postgres pair in the repo rather than a wiki page. The strongest argument against it is the maintainership notice — a project asking for volunteers is a project you should depend on with a plan B, which in this case is crystal-db plus your own structs.
Pitfalls and Gotchas
- Placeholder syntax is driver-specific.
?for SQLite and MySQL,$1/$2for PostgreSQL. Copying a query between drivers without changing placeholders is the most common silent failure in Crystal database code. - Crystal is compiled, so schema drift breaks at build time or at 3 a.m. A model with a
Stringcolumn against a nullable database column compiles fine and explodes on a realNULL. Make database columns match your type nilability, not the reverse. - Jennifer’s MySQL ceiling is a hard limit. If you are on MySQL 8.0.36+, do not plan a migration to Jennifer without checking the issue tracker first.
- Granite needs a second tool for schema. Adding micrate is not optional if you want versioned migrations; the ORM does not generate them.
- Acknowledged single-maintainer risk. Both ORMs depend heavily on one or two people. Either is fine as a dependency, but pin versions in
shard.lockand commit that file — Crystal’s shard resolution is reproducible only if you keep the lock. - Pool size defaults bite under load.
DB.openpools connections, and an under-sized pool turns into latency that looks like database slowness. Set the pool size explicitly and monitor it, exactly as you would for any other PostgreSQL client. - Do not map everything. Wrapping every table in a model forces the ORM to materialise objects you never use. For reporting endpoints, drop to
db.queryand read columns directly.
FAQ
Is Granite or Jennifer abandoned? Neither is abandoned today, but both carry maintenance risk signals worth reading. Granite’s README explicitly asks for maintainers, and its last push was 2026-08-05. Jennifer’s last push was 2025-01-25, with a documented gap on MySQL 8.0.36 and newer. crystal-db is the only layer in this comparison that is maintained as part of the official Crystal organisation.
Can I mix crystal-db and an ORM in one project?
Yes, and it is a common pattern. The ORM sits on top of crystal-db, so both share the same drivers and pooling. Teams typically use the ORM for CRUD and drop to db.query for reporting or bulk operations inside the same binary.
Which one has the best migration story?
Jennifer, because migrations are Crystal classes with an explicit up and down and are part of the same tool. Granite delegates to micrate and plain SQL files, which some teams prefer because the SQL is visible. crystal-db has no migration system at all — you choose one.
What about connection pooling? All three share crystal-db’s pool, so the tuning knobs are identical. The design question is sizing: high-throughput Crystal services often benefit from a smaller pool plus an external pooler, as described in our PgBouncer, Pgpool and Odyssey comparison.
Does any of this change what framework I should use? If you have not picked a framework yet, our Crystal web framework comparison covering Kemal, Lucky and Amber is the better starting point — Amber pairs with Granite, Kemal is framework-agnostic, and either can run on crystal-db directly. The same trade-offs appear in other ecosystems too: see our Rust database library comparison of Diesel, SeaORM and rusqlite for a different language’s answer to the same problem.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com