В предыдущей публикации я описал базовые операции при работе с MongoDB в Java.
Использовать org.bson.Document для работы с данными, хранящимися в коллекциях далеко не всегда удобно. Как правило, в коллекциях хранятся данные имеющие какую-то определённую структуру, соответствующую, например, POJO-классам вашего проекта.
Для реализации преобразования BSON в объекты соответствующих классов и обратно в драйвере MongoDB предусмотрены кодеки, реализующие интерфейс org.bson.codecs.Codec. В этой публикации я рассмотрю варианты разработки собственного кодека и использования готовых.
POJO-классы проекта
Структуру данных, использовавшуюся мной в предыдущей публикации можно описать в виде простого класса:
Чтобы пример был более сложным и наглядным, я добавил лист со списком комментариев и список тэгов в свойства класса Todo. Класс TodoComment выглядит следующим образом:
Использование стандартных кодеков
Клиентская библиотека MongoDB содержит достаточно большое количество кодеков, которые могут решить большую часть потребноестей проекта. Для нашего примера понадобятся три провайдера кодеков:
- PojoCodecProvider — для сериализации POJO-классов
- Jsr310CodecProvider — для сериализации типов Java 8 Date/Time API
- ValueCodecProvider — для сериализации простых типов
Для подключения кодеков можно использовать метод withCodecRegistry(), вызываемый при получении базы данных или коллекции. Пример использования стандартных кодеков:
Обратите внимание, что PojoCodecProvider конфигурируется при помощи класса-билдера, метод register() которого регистрирует POJO-классы, для которых должны применяться сериализация и десериализация.
Собственный кодек
Для большинства случаев стандартных кодеков будет достаточно, но по тем или иным причинам вы можете захотеть реализовать собственные кодеки.
Для сериализации и десериализации каждого класса должен существовать собственный класс-кодек, реализующий интерфейс org.bson.codecs.Codec, как уже было сказано выше. Этот интерфейс в свою очередь расширяет интерфейсы org.bson.codecs.Encoder и org.bson.codecs.Decoder, в результате чего разработчику нужно реализовать три метода: encode() — для сериализации объекта POJO-класса в BSON, decode() — для десериализации и getEncoderClass() — для определения, для сериализации объектов какого класса должен использоваться этот кодек.
Сериализация
При сериализации объекта в обязательном порядке нужно указать начало и конец документа:
Сериализация свойств объекта выполняется методами write…() объекта типа org.bson.BsonWriter и представляет собой сохранение пар ключ-значение в BSON-документе. Имя свойства может быть записано отдельно при помощи метода writeName():
В приведённом выше примере имя свойства dateCreated сериализуется вызовом метода writeName(), а его значение — вызовом метода writeDateTime(), в то время как пара ключ-значение content сериализуется вызовом лишь одного метода — writeString().
При сериализации массивов простых типов потребуется указать границы массива и сериализовать его элементы в цикле:
Немного иначе обстоят дела со вложенными объектами и массивами вложенных объектов. С одной стороны сериализацию/десериализацию вложенных объектов можно реализовать в этом же методе, но в большинстве случаев для этого логичнее использовать соответствующие кодеки:
Десериализация
Процесс десериализации практически полностью аналогичен процессу сериализации, однако есть свои нюансы:
Если использовать такой способ десериализации, то можно столкнуться с следющими проблемами:
- Порядок свойств в документе не гарантирован
- Наличие свойств тоже не гарантировано
- Значение свойства может иметь значение null
Исходя из этих проблем, логичнее всего будет организовать таким образом чтобы десериализация каждого свойства в следующем порядке:
- Получить тип свойства методом BsonReader.readBsonType()
- Получить имя свойства методом BsonReader.readName()
- Получить значение при помощи
- Методов BsonReader.read…(), если тип свойства простой
- Метода DecoderContext.decodeWithChildContext(), если нужно десериализовать вложенный объект
- Метода BsonReader.readNull(), если тип свойства — BsonType.NULLПолучить значение при помощи:
Пример десериализации с учётом вышеуказанных проблем:
Полный код классов TodoCodec и TodoCommentCodec:
Пример использования кодеков: