Как устроен Primary Key

После долгой работы с реляционными БД и, в частности, с SQL, переход на документную базу MongoDB не так прост, как кажется. Надо немного перестроить менталитет. Это как с PHP/Python перейти на Node.js — привыкнуть к асинхронности и колбэкам требует времени.

Так и с NoSQL, в частности с MongoDB. Первое что бросается в глаза — это необычные ID. Это не автоинкремент, как в других базах. Более того, из коробки вообще нет автоинкрементов и их нужно реализовывать самому, если они нужны.

Начнем с ObjectId. Кажется что это хеш и как же делать поиски вида:

SELECT * FROM t WHERE id > 1234

На самом деле в MongoDB так же можно строить запросы по _id (поле айди в монге с префиксом):

db.t.find({ _id: { $gt: ObjectId('5e1b270142dcae0010186f02') } })

И так же по этому полю можно сортировать.

Не все знают, но это не просто хеш, это функция от времени, и хеш можно привести к DateTime. И делается это достаточно просто (пример на JS):

function objectIdToDateTime(objectId) {
return new Date(parseInt(objectId.substring(0,8),16)*1e3)
}

Пробуем наш айдишник:

objectIdToDateTime('5e1b270142dcae0010186f02')
// Sun Jan 12 2020 17:02:41 GMT+0300

В самой монге есть возможность получить время от ObjectId:

ObjectId("5e1b270142dcae0010186f02").getTimestamp()
// ISODate("2020-01-12T17:02:41.000+03:00")

Если хочется сгенерить ObjectId, то нужно сделать обратное преобразование:

function generateObjectId() {
return Math.floor((new Date).getTime()/1e3)
.toString(16) + '0'.repeat(16)
}

Тут мы генерим первую часть хеша, но вот конец хеша у нас состоит из нулей. Оригинальный ObjectId во второй части содержит некий рандомный хеш. Мы можем использовать алгоритм для генерации UID:

function generateObjectId() {
return Math.floor((new Date).getTime()/1e3).toString(16) +
(('x'.repeat(16).replace(/x/g,
_=>(Math.random()*16|0).toString(16))
))
}

Варианты получения UID можно прочитать в статье

Простой способ генерации символьных ID и UUID
На JavaScript Если вдруг нужно быстро сгенерить символьные ID, которые представляют собой сочетание цифр и символов, при этом вам не нужна высокая надежность и коллизии вам не страшны, то можно сделать это очень быстро следующим образом: const id = `f${(~~(Math.random()*1e8)).toString(16)}`; // Res…

Прелесть ObjectId в том, что не так просто вычислить следующий ID, в отличие от автоинкремента. Так же нельзя понять сколько в базе записей. Но зато можно получить дату создания записи.

А еще плюсом такой генерации Primary Key заключается в том, что мы можем получить PK до того, как запишем данные в базу. Таким образом удобно строить различные ORM, позволяющие создавать полноценный объект коллекции, до его записи в базу. Конечно в SQL базах так же можно получить last id и все такое, но это будет сложнее и потребует транзакций, чтобы он не успел измениться. Здесь же мы даже не обращаемся к базе.

В следующем посте разберем как создавать автоинкременты в MongoDB.