Glossary

ORM

An ORM (Object-Relational Mapper) maps database tables to classes and rows to objects, providing a programmatic interface to query and manipulate data using your programming language instead of raw SQL.

Explanation

ORMs abstract away SQL: instead of db.query('SELECT * FROM users WHERE id = $1', [id]), you write User.findByPk(id) (Sequelize) or prisma.user.findUnique({where: {id}}) (Prisma). The ORM generates and executes the SQL on your behalf. This reduces boilerplate for CRUD operations and keeps your codebase in one language (no SQL strings embedded in JavaScript). Popular ORMs: Prisma (TypeScript-first, schema-driven, generated type-safe client — the modern choice for TypeScript projects), Sequelize (mature, flexible, supports many databases), TypeORM (TypeScript decorators, similar to Hibernate in Java), Drizzle (lightweight, SQL-like TypeScript ORM — close to SQL without raw strings), and Mongoose (schema-based MongoDB ODM). The N+1 query problem is ORMs' most notorious footgun: fetching a list of posts and then loading each post's author with a separate query (one for each post) produces n+1 database queries. ORMs have escape hatches — eager loading, includes, select relations — but you must know to use them. This is the vibe coder's trap: User.findAll() looks fine until your "getAllUsers" endpoint makes 201 database queries for 200 users. When to bypass the ORM: complex queries with multiple JOINs, aggregations, window functions, CTEs, and database-specific features are often cleaner and more maintainable as raw SQL. ORMs generate suboptimal SQL for complex queries. Prisma.raw, Sequelize.query(), and Knex.js (query builder — SQL-like without full ORM overhead) are good tools for these cases.

Code Example

javascript
// Prisma ORM example (TypeScript)

// schema.prisma defines your data model
// model User {
//   id    Int    @id @default(autoincrement())
//   email String @unique
//   posts Post[]
// }
// model Post {
//   id     Int    @id @default(autoincrement())
//   title  String
//   userId Int
//   user   User   @relation(fields: [userId], references: [id])
// }

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// CRUD with generated type-safe client
const user = await prisma.user.findUnique({ where: { id: 42 } });
const users = await prisma.user.findMany({
  where: { active: true },
  orderBy: { createdAt: 'desc' },
  take: 10,
  skip: 0,
});

// Eager loading — avoids N+1 (loads posts in same query)
const usersWithPosts = await prisma.user.findMany({
  include: { posts: { take: 5, orderBy: { createdAt: 'desc' } } },
});
// Generates: SELECT users.*, posts.* FROM users LEFT JOIN posts... LIMIT 5

// Raw SQL for complex queries
const result = await prisma.$queryRaw`
  SELECT u.name, COUNT(p.id) AS post_count
  FROM users u LEFT JOIN posts p ON u.id = p.user_id
  GROUP BY u.id, u.name
  HAVING COUNT(p.id) > 5
`;

Why It Matters for Engineers

ORMs are the default database layer in most web frameworks. Knowing what SQL your ORM generates — and when it's generating N+1 queries, missing indexes, or ignoring transactions — is essential for production performance. The engineers who "just use the ORM" and don't look at the generated queries routinely ship APIs with 100x more database queries than necessary. Understanding ORMs also means understanding their migrations: Prisma migrate, Sequelize migrations, TypeORM migrations — these are the tools that evolve your database schema safely in production. Running migrations incorrectly can cause downtime or data loss.

Related Terms

SQL · Relational Database · Transaction · JOIN

Learn This In Practice

Go deeper with the full module on Beyond Vibe Code.

Databases Fundamentals → →