VillageSQL is a drop-in replacement for MySQL with extensions.
All examples in this guide work on VillageSQL. Install Now →
The Settings That Matter Everywhere
Whatever the framework, three things must be true:- The connection uses
utf8mb4, or emoji break in transit. See utf8mb4 and emoji. - Strict SQL mode is on (it is the MySQL 8.x default), so bad data errors instead of silently truncating.
- The pool size is deliberate. Every framework opens a pool per process; multiply by your process count before picking a number. See Connection pooling.
Django
- Use
mysqlclient, the maintained C driver Django’s docs recommend. - Django emulates some constraints in Python; add real database constraints too (
unique=Truebecomes a real unique index, but check constraints needCheckConstraintinMeta). select_related()(JOIN) andprefetch_related()(second query) are the N+1 tools; see below for when.
Rails
encoding: utf8mb4indatabase.ymlsets both the connection charset and the default forrails db:create.- ActiveRecord validations (
validates_uniqueness_of) race under concurrency; back every uniqueness validation with a real unique index, and let the 1062 duplicate-key error be the last line of defense. includesis the N+1 tool;strict_loadingmode turns lazy loading into an error so N+1s fail in development instead of shipping.- Rails 8.1 generates
max_connectionsindatabase.ymlwhere older versions generatedpool. Apool:key still works, silently, as the fallbackmax_connectionsreads; setting both to different values raises at boot. See Rails 8 with MySQL for a full app, including the--database=mysqlflag Rails 8 needs since it no longer defaults there.
Prisma
- Prisma speaks utf8mb4 by default; the setting to watch is
connection_limitin the URL, which defaults low. prisma migrategenerates DDL from schema drift. Read the generated SQL before applying to production, exactly as you would a hand-written migration. See Schema migrations.- Relation queries use
include; the N+1 shape appears when you loop over results and access relations one by one. - Prisma 7 moves the connection URL above out of
schema.prismaand into a separate config file, and requires a driver adapter to connect. See Next.js and Prisma with MySQL for a full Prisma 7 app.
The N+1 Problem, Once
The universal ORM performance bug: load 100 orders, then lazily load each order’s customer, and the ORM issues 101 queries. The fix is the same idea in every framework — declare the relations you need up front so the ORM fetches them in one or two queries (select_related / includes / include). The detection tool is also universal: turn on query logging in development and read what the ORM actually sends, or watch the slow query log in production.
When to Drop to SQL
ORMs earn their keep on CRUD and lose it on analytics. Window functions, CTEs, bulk upserts, and multi-table reporting queries are clearer in SQL than in a query-builder chain trying to express them. Every ORM has an escape hatch (raw() in Django, find_by_sql in Rails, $queryRaw in Prisma); using it for the 5 percent of queries that deserve it is good engineering, not defeat. It is also how VillageSQL extension functions reach an ORM application: SELECT UUID_V7() or ai_prompt(...) work through any of these escape hatches, since the ORM just passes the SQL through.
See also
- Next.js and Prisma with MySQL — a full app on Prisma 7, not just the config
- Next.js and Drizzle with MySQL — a full app on Drizzle, a lighter, SQL-first alternative not covered above
- Next.js with MySQL — connection setup and query patterns with no ORM at all
- Rails 8 with MySQL — a full app, including the scaffold flag Rails 8 needs to pick MySQL at all
- Laravel 12 with MySQL — a full app on Laravel’s Eloquent ORM, not covered above
- Go and sqlc with MySQL — generated, type-safe queries instead of a runtime ORM
- Connection pooling — sizing pools across processes
- utf8mb4 and emoji — the charset setting every config above sets
- Slow query log — catching what the ORM generates
- Schema migrations — applying ORM-generated DDL safely

