Skip to content

Intermediate · NoSQL, ORMs and data features · topic 28 of 54 · 30 min

ORMs: working with databases from code

Explanation

An ORM (Object-Relational Mapper) lets you read and write database rows as normal objects in your programming language. It writes the SQL for you, keeps your schema in migrations and saves a lot of typing. You still need to understand SQL, because careless ORM code can fire hundreds of queries where one would do.

What an ORM does

Your code works with objects, like { id: 7, name: "Asha" }. The database works with tables and SQL. An ORM (Object-Relational Mapper) sits between them: you call a method such as user.findMany(), and it builds the SQL, runs it and turns the rows back into objects.

Think of it like this: An ORM is like a translator at a meeting. You speak JavaScript, the database speaks SQL. The translator saves you from learning every word, but if you speak carelessly, the translator will repeat your mistake fifty times.
LanguageORM or query builderNotes
TypeScript / Node.jsPrismaSchema file, generated type-safe client, built-in migrations
TypeScript / Node.jsDrizzleLight, SQL-like syntax, schema written in TypeScript
TypeScript / Node.jsTypeORM, SequelizeOlder, class or model based; common in existing projects
PythonSQLAlchemy, Django ORMSQLAlchemy for any app; Django ORM comes with Django
JavaHibernate (JPA)Standard in Spring Boot projects
PHPEloquentComes with Laravel
C# / .NETEntity Framework CoreStandard in ASP.NET Core

Models

A model describes one table: its name, its columns and their types, and how it links to other tables. In Prisma you write models in a schema file; in Drizzle, SQLAlchemy or Django you write them as code.

output
model Customer {
  id     Int     @id @default(autoincrement())
  name   String
  city   String?
  orders Order[]
}

model Order {
  id         Int      @id @default(autoincrement())
  amount     Int
  customer   Customer @relation(fields: [customerId], references: [id])
  customerId Int
}
Two Prisma models: one customer has many orders.
javascript
// Example only: Prisma Client (needs a database)
const puneCustomers = await prisma.customer.findMany({
  where: { city: 'Pune' },
  orderBy: { name: 'asc' },
  take: 20,
});

const order = await prisma.order.create({ data: { amount: 450, customerId: 2 } });

// Drizzle: the same idea, closer to SQL
const rows = await db.select().from(customers).where(eq(customers.city, 'Pune'));
Reading and creating rows with an ORM. The ORM writes the SQL.

Migrations

A migration is a small, numbered file that changes the database schema: create a table, add a column, add an index. Migrations are kept in Git and run in order on every machine, so your laptop, your teammate and the production server all end up with the same tables. Examples: prisma migrate dev, drizzle-kit generate, Alembic for SQLAlchemy, Django makemigrations.

The N+1 query problem

N+1 happens when you load a list with one query, then run one more query for each item in a loop. For 50 orders that is 1 + 50 = 51 trips to the database. Each trip is small, but together they make the page slow.

output
N+1 (slow)                                  Fixed (fast)
-----------                                 ------------
Q1:  SELECT * FROM orders LIMIT 50          Q1: SELECT orders.*, customers.name
Q2:  SELECT * FROM customers WHERE id = 2       FROM orders
Q3:  SELECT * FROM customers WHERE id = 1       JOIN customers
Q4:  SELECT * FROM customers WHERE id = 3         ON customers.id = orders.customer_id
...                                             LIMIT 50
Q51: SELECT * FROM customers WHERE id = 9

51 round trips                              1 round trip
The N+1 picture: a query inside a loop, versus one JOIN.
javascript
// N+1: one query per order inside the loop
const orders = await prisma.order.findMany({ take: 50 });
for (const order of orders) {
  order.customer = await prisma.customer.findUnique({ where: { id: order.customerId } });
}

// Fixed: ask the ORM to load the relation in the same request
const ordersWithCustomers = await prisma.order.findMany({
  take: 50,
  include: { customer: true },
});
An await inside a loop over rows is the warning sign. Use include / joins instead.

When to write raw SQL

  • Big reports with GROUP BY, window functions or complex joins.
  • Bulk updates of thousands of rows in one statement.
  • A slow query that you have measured and want to tune by hand.
  • Database features the ORM does not support yet.
Careful: Raw SQL must still use parameters (placeholders like ? or $1), never string concatenation of user input. Prisma $queryRaw with a tagged template and Drizzle sql`` do this for you.
Tip: Turn on query logging in development (for example new PrismaClient({ log: ["query"] })). Seeing the real SQL is the fastest way to spot N+1 and missing indexes.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

ORMs: working with databases from code · 1/7

ORMs

Writing SQL by hand for every screen gets tiring. Today we look at ORMs, which let you use objects instead.

Prefer a video by a teacher? Find free videos on “prisma orm tutorial for beginners” on YouTube ↗

In real life: A slow "My orders" page in a shopping app

A small online saree shop showed 50 orders per page with the customer name and city. Each page took two seconds. Query logging showed 51 queries: one for the orders and one per order for the customer. Changing the ORM call to include the customer turned it into a single JOIN, and the page loaded in under 100 milliseconds. No new server was needed, just one line of code.

Practice (0/2 done)

Task 1 · Code · SQL

The app ran one query for orders and then one query per order to get the customer name (N+1). Replace it with ONE query: return the order id, the customer name and the amount for every order, ordered by order id. Tables: customers(id, name, city) and orders(id, customer_id, amount).

Task 2 · Code · Javascript

Write a tiny query builder buildSelect(table, where) that returns { sql, params }. For buildSelect("users", { city: "Pune", active: 1 }) it returns { sql: "SELECT * FROM users WHERE city = ? AND active = ?", params: ["Pune", 1] }. With an empty where it returns "SELECT * FROM users" and params []. Values must go into params, never into the SQL text. Table and column names cannot be placeholders, so throw an Error if any of them does not match /^[a-z_][a-z0-9_]*$/i.

Quick quiz

Question 1

1.What does an ORM do?

Question 2

2.A page loads 30 posts, then runs one query per post to get its author. How many queries is that?

Question 3

3.What is a migration?

Question 4

4.Which ORM comes with the Laravel PHP framework?

Question 5

5.You need raw SQL for a report. How should you include the user's chosen city?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does an ORM do?

Answer: Maps rows to objects and writes SQL for you

An ORM translates between objects in your code and rows in the database, generating the SQL.

A page loads 30 posts, then runs one query per post to get its author. How many queries is that?

Answer: 31

1 query for the posts plus 30 for the authors: 31. This is the N+1 problem.

What is a migration?

Answer: A versioned file that changes the database schema

Migrations are numbered schema changes kept in Git, so every environment gets the same tables.

Which ORM comes with the Laravel PHP framework?

Answer: Eloquent

Eloquent is Laravel's ORM. Hibernate is Java, SQLAlchemy is Python and Drizzle is TypeScript.

You need raw SQL for a report. How should you include the user's chosen city?

Answer: As a parameter with a placeholder

Parameters keep values separate from SQL code, which prevents SQL injection.

What does an ORM do?Related

Answer: Maps objects in code to rows in database tables

Object-Relational Mapper.

From Entity Framework Core: databases with C#

What is a database migration?Related

Answer: A numbered file that changes the schema by one step, applied in order

Migrations are small, ordered schema changes kept in the repository, so every database can be built the same way.

From Creating the database: schema, migrations and seed data

Which is the safe way to change a production database schema?Related

Answer: Reviewed migration scripts with Flyway or Liquibase

Migrations are versioned and reviewed.

From JPA and Spring Data repositories

What causes SQL injection?Related

Answer: Joining user input directly into SQL text

When user text is glued into the query, it can change the query itself. That is injection.

From SQL injection and how to stop it

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “NoSQL, ORMs and data features” before its exam: Pagination: sending data page by page, Handling file uploads safely.
  2. Exam: NoSQL, ORMs and data features · 14 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Authentication and authorization. 54 topics left in the course before the final exam.

← Previous: NoSQL and MongoDB