findOneAndUpdate functions

Updating is the most challenging issue in Lesan. Because by updating one document, thousands or maybe millions of other documents may also be updated. Don't worry, this is done automatically by Lesan. And also for complex scenarios where millions of documents need to be updated, we have created various solutions.

Let's explore a few scenarios.

  • 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,
});

When we were defining user relationships, we said that each user has a relationship with the cities he lived in. In defining this relationship, we said that we store the last 50 users who lived in these cities in the city table. So it is possible that the user who is updated may be in the list of these 50 users in several cities. So we have to find the cities where this user lives and update the list of users in those cities.
If we set the order of saving this list of 50 items based on a field from the user and the same field has been updated, the story will be a little more complicated. Because it is possible that if this user is in the list of 50, he will be removed from this list due to the updating of this field and another user who has suitable conditions will replace this user. For more information please read here.

All things will be the same for the country.

You can find full example here and test the findOneAndUpdate method in local computer.

before executing mainuserupdateUser: db-user-update

executing mainuserupdateUser: db-user-updating

after executing mainuserupdateUser: db-user-updated

Add E2E Test

Like before, for adding updateUser request to E2E section you should click on the e2e button (like bottom picture) to add your request to E2E section.

e2e sequence

Then, in the E2E section and updateUser sequence, you should replace the user id that you set capture in own sequence with default user id. default user id in updateUser sequence is like below picture.

e2e sequence

The replaced user id is like below picture.

e2e sequence
  • 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,
});

If the country is updated in a real scenario, the number of documents that need to be updated along with the country is likely to be very large. Because all users and cities of that country must be updated.

You can find full example here and test the findOneAndUpdate method in local computer.

before executing maincountryupdateCountry: before-update-country

executing maincountryupdateCountry: updating-country

after executing maincountryupdateCountry: after-update-country

Add E2E Test

Like before, for adding updateCountry request to E2E section you should click on the e2e button (like bottom picture) to add your request to E2E section.

e2e sequence

Then, in the E2E section and updateCountry sequence, you should replace the country id that you set capture in own sequence with default country id. default country id in updateCountry sequence is like below picture.

e2e sequence

The replaced country id is like below picture.

e2e sequence

Lesan's solution to the update challenge

As you have seen, we face a big problem for updating the country and millions of documents may need to be updated by updating one document. This is the biggest challenge in Lesan and we propose 3 solutions to solve this problem:

QQ solutions

QQ stands for query queue, a queue of commands to be sent to the database. We use QQ to chunk millions of updates. In this way, we take a section of several hundred thousand that we guess should be updated immediately and perform the update, then we store the ID of the last updated document along with the necessary commands for updating in QQ. And we do another part of the update whenever hardware resources are low on contention.

In-memmory DB solutions

Because we have detailed relationship information, we can know when sending information to the customer that the information sent has changes in QQ. As a result, by saving the changes in an in-memory database, we can correct the sent information in the same RAM and send it to the client, without changing the actual data in the hard disk.

Make new relation solutions

Perhaps the most beautiful thing about database relations is here.
In Lesan, after checking the data model, we can convert frequently changing fields into a new relationship.
Going back to the previous example, our real problem was updating a country because millions of documents needed to be updated.
In our example, each country had the following pure fields:

  • name
  • abb
  • population

The name and abb fields usually do not change, in fact, in our example, they never change, but the population field may change a lot, imagine that we want to have the updated population of each country every second, that is, all countries are updated every second. And by updating each country, all the cities and users belonging to that country must be updated. Well, this is almost impossible.
But if we convert the population field into a new schema and create a relationship with the country for it, all problems will be solved.
Pay attention to the following code:

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: "country",
    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 we can remove the population field from both the pure fields of the city and the pure fields of the country. But there is still the population field in both the country and the city, and we can write any type of filter or sort for the city and country schemas based on the population. Even better, now we have a list of the last 50 population records for each country and each city, and we can find, for example, countries that have added more than 100 people to their population in the last minute, without sending a complex query to the database.

And the most important thing is that now, with the population change, instead of millions of documents, only two documents change.
Because to change the population, we make a new record for the population and store this new record only in the country or city that belongs to it in the populations list. And we no longer need to update thousands of cities or millions of users.