findOneAndUpdate
Updating is the most challenging part of Lesan. Updating one document may require updating thousands โ or even millions โ of other documents. The good news: Lesan does it automatically. This tutorial walks through the two scenarios (updating a user, updating a country), then explains the three solutions Lesan proposes for the worst case.
Best case: update a userโ
const updateUserValidator = () => {
return object({
set: object({
_id: objectIdValidation,
name: optional(string()),
age: optional(number()),
}),
get: coreApp.schemas.selectStruct("user", 1),
});
};
const updateUser: ActFn = async (body) => {
const { name, age, _id } = body.details.set;
const setObj: { name?: string; age?: number } = {};
name && (setObj.name = name);
age && (setObj.age = age);
return await users.findOneAndUpdate({
filter: { _id: new ObjectId(_id) },
projection: body.details.get,
update: { $set: setObj },
});
};
coreApp.acts.setAct({
schema: "user",
actName: "updateUser",
validator: updateUserValidator(),
fn: updateUser,
});
Remember the user model: each user lives in cities, and each city stores the last 50 users in its users list. So when a user is updated, Lesan must find every city that user lives in and update the embedded users list in each one.
If a related list is sorted by a field that the update changes, it gets more interesting: the user may drop out of the list entirely, and another qualifying user takes their place. Lesan handles that refill automatically. For the full mechanics see What Is the Relationship Really?.
findOneAndUpdate accepts:
filterโ the MongoDB filter selecting the document to update.updateโ the MongoDB update document (e.g.{ $set: {...} }).projectionโ shapes the returned document.options(optional) โ MongoDBFindOneAndUpdateOptions.
The full example is examples/document/08-1-findOneAndUpdate.ts.
Before updating the user:
While updating:
After updating โ the user's name and age changed, and the embedded lists were kept in sync:
Worse case: update a countryโ
const updateCountryValidator = () => {
return object({
set: object({
_id: objectIdValidation,
name: optional(string()),
abb: optional(string()),
population: optional(number()),
}),
get: coreApp.schemas.selectStruct("country", 1),
});
};
const updateCountry: ActFn = async (body) => {
const { name, abb, population, _id } = body.details.set;
const setObj: { name?: string; abb?: string; population?: number } = {};
name && (setObj.name = name);
abb && (setObj.abb = abb);
population && (setObj.population = population);
return await countries.findOneAndUpdate({
filter: { _id: new ObjectId(_id) },
projection: body.details.get,
update: { $set: setObj },
});
};
coreApp.acts.setAct({
schema: "country",
actName: "updateCountry",
validator: updateCountryValidator(),
fn: updateCountry,
});
In a real scenario, updating a country means updating a very large number of documents: every user and every city that embeds this country. The full example is examples/document/08-2-findOneAndUpdate.ts.
Before updating the country:
While updating:
After updating โ every city and user that embeds the country was updated too:
Lesan's solutions to the update challengeโ
As you've seen, updating one country can cascade into millions of documents. Lesan proposes three approaches:
QQ โ the query queueโ
QQ stands for query queue: a queue of commands to send to the database. Use it to chunk millions of updates. Take the first few hundred thousand documents that should be updated, perform the update immediately, then store the id of the last updated document together with the remaining commands in QQ. Run the next chunk whenever hardware resources are free.
In-memory databaseโ
Because Lesan keeps detailed relation information, it can know โ when sending data to the client โ that some of it changed in QQ. By saving those changes in an in-memory database, the response can be corrected in RAM and sent to the client without touching the data on disk at all.
Make a new relationโ
Perhaps the most elegant solution: turn frequently-changing fields into a new relation.
Back to our example: a country's name and abb rarely change, but population might be updated every second โ which would force every city and user to update every second. Impossible at scale.
Instead, move population into its own schema and relate it to country (and city):
const populationPure = {
population: number(),
type: enums(["City", "Country"]),
};
const populationRelations = {
country: {
optional: false,
schemaName: "country",
type: "single" as RelationDataType,
relatedRelations: {
populations: {
type: "multiple" as RelationDataType,
limit: 50,
sort: {
field: "_id",
order: "desc" as RelationSortOrderType,
},
},
},
},
city: {
optional: false,
schemaName: "city",
type: "single" as RelationDataType,
relatedRelations: {
populations: {
type: "multiple" as RelationDataType,
limit: 50,
sort: {
field: "_id",
order: "desc" as RelationSortOrderType,
},
},
},
},
};
const populations = coreApp.odm.newModel(
"population",
populationPure,
populationRelations
);
Now remove population from the pure fields of city and country โ but you can still filter and sort country/city based on the population relation, since the related snapshots are embedded. Even better, each country keeps a list of its last 50 population records, so you can ask questions like "which countries added more than 100 people in the last minute" without a complex query.
And the crucial payoff: a population change now updates two documents instead of millions. Each population change creates one new population record and stores it only in its country's (or city's) populations list. No more cascading updates.
Next stepsโ
- deleteOne โ deletion has the same challenges as update, solved the same way
- Queries & Projections โ the full API reference