Read replicas
Configure read-only connections beside the primary and MikroORM's connectionType option routes
individual reads to them.
const orm = await MikroORM.init({
clientUrl: 'bolt://primary:7687',
user: 'neo4j',
password: 'password',
replicas: [
{ clientUrl: 'bolt://replica-1:7687', user: 'neo4j', password: 'password' },
{ clientUrl: 'bolt://replica-2:7687', user: 'neo4j', password: 'password' },
],
});
connectionType is honoured on every read — find, findOne, count, virtual entities, streams,
and the relation loads behind populate:
const books = await em.find(Book, {}, { connectionType: 'read' });
const total = await em.count(Book, {}, { connectionType: 'read' });
const cursor = em.stream(Book, { connectionType: 'read' });
Three rules
- Absence means the primary. Replica reads are opt-in per query, never a default. A replica lags by an unbounded amount, and turning every find eventually-consistent on a caller's behalf is not a trade the driver gets to make.
- A populated read stays on one connection. Root and relations go to the same place, so one logical read is not spread across two servers with different lag.
- A transaction wins outright. Inside
ctxthe session belongs to the connection the transaction was opened on. Honouring the option there would run the statement outside the transaction — unable to see its uncommitted writes, and untouched by its rollback — so within a transaction the option is not so much overridden as unrepresentable.
Writes never consult it; core does not declare it on a write's options at all.
Connection selection never reaches the statement. The same find compiles to identical Cypher and identical parameters whichever connection it is sent on — the route is a property of sending a query, not of building one.
Manual control
const readConn = em.getDriver().getConnection('read');
const writeConn = em.getDriver().getConnection('write');
// The live `neo4j-driver` instance behind either one.
const client = await writeConn.getClient();
getClient() is the escape hatch to the Bolt driver itself — the same shape the other MikroORM
drivers expose as getKnex() / getClient(). It is how a library outside the ORM shares this
connection instead of opening a second pool; serving a generated GraphQL API
is the case it exists for. The connection owns the driver, so orm.close() closes it and you should
not.
Access mode
A read-only transaction opens its session in read access mode; anything else opens in write mode.
On a cluster the routing driver can send a read-mode session to a follower, and a stray write inside
one is refused by the server — surfacing as MikroORM's ReadOnlyException rather than a raw Neo4j
error.
await em.transactional(async (em) => em.find(Book, {}), { readOnly: true });
Absence means write, which is the safe direction to guess. A declared routine is refused earlier still — before a statement is sent — because its declaration says what access it needs.
That is the whole trade you are making by asking for one. Read-after-write within a single request is the case that bites; leave those on the primary.