Consider this TypeORM call:
await entityManager.save(User, users);
A simple question: does this execute one INSERT per row, or one INSERT
containing every row?
The API doesn’t tell you. To find out, you have to dig into EntityManager’s implementation:
- Its save() method interface
- Its corresponding concrete implementation, and its cascade of dispatchers:
Five files and a dozen hops later, you have an answer.
Maybe.
Whether TypeORM actually emits a bulk insert depends on internal state and hidden conditions. If you don’t know those rules off the top of your head, you’re forced to audit framework internals to answer a basic question about your own system.
Now compare that to this:
await db.query(
`INSERT INTO users (name, email)
VALUES ($1, $2), ($3, $4), ($5, $6)`,
[...]
);
// or with bulk parameterisation
await db.query(
`INSERT INTO users (name, email)
SELECT * FROM UNNEST($1::text[], $2::text[])`,
[names, emails]
);
There’s no magic here. The code tells you exactly what query is going over the wire. The SQL isn’t buried behind an object graph or an execution engine.
This is the fundamental trade-off of an ORM.
It removes raw SQL from your codebase, but it can’t remove database realities from your problem domain. Eventually, you have to care about query counts, locks, batching, and query plans. When that happens, the abstraction doesn’t eliminate complexity, it just moves it somewhere harder to see.
And instead of reading your own code to figure out what’s happening, you’re reading the framework’s.
(And don’t get me started on lazy loading triggering accidental N+1 queries. That’s a whole separate essay on invisible performance traps.)