Приветствуем, Хаброжители!
Мы — компания «Центр информационных технологий», создаем инфраструктурные решения и высокотехнологичные программные продукты, поддерживающие глобальные государственные инициативы в Российской Федерации и странах Евразийского экономического союза.
В этом посте мы поделимся с вами нашим опытом в области мобильной разработки приложений, ориентированных на работу с данными.

При разработке мобильных приложений, ориентированных на работу с данными, мы часто сталкиваемся с необходимостью полноценной офлайновой работы приложения, а также синхронизации изменений в данных между мобильным приложением и бэкэндом, с которым также работают пользователи десктопных или веб-приложений.
Использование «облаков» для синхронизации данных далеко не всегда позволительно, особенно если дело касается заказчиков высокого уровня, которые не допускают такого решения из соображений безопасности и требуют развёртывания всех компонентов системы in-house. В этой статье я расскажу о нашем опыте решения этой задачи с помощью связки полноценной серверной базы данных Couchbase и «облегчённой» мобильной базы данных Couchbase Lite.
База данных Couchbase — документная распределённая NoSQL-база данных, обеспечивающая высокую производительность за счёт записи данных в первую очередь в оперативную память и уже потом (eventual persistence) на дисковое хранилище. Благодаря независимости и равноправию узлов с привязкой документа к конкретному узлу, Couchbase обеспечивает сильную целостность (strong consistency). Запросы к Couchbase строятся на основе индексированных представлений, реализующих модель вычислений MapReduce.
База данных Couchbase Lite — это легковесная версия Couchbase, предназначенная для использования в десктопных и мобильных приложениях и поддерживающая синхронизацию с серверной базой Couchbase. Реализации этой базы данных есть под iOS, Android, Java и .NET, так что её можно использовать не только в мобильных, но и в десктопных приложениях. Стоит упомянуть, что реализация Couchbase Lite под iOS на текущий момент обладает рядом преимуществ по сравнению с остальными платформами — например, там есть возможность полнотекстового поиска, а также средства для автоматического маппинга документов в объектные модели.
Для синхронизации Couchbase и Couchbase Lite используется протокол репликации, «совместимый» с протоколом CouchDB. Совместимый в кавычках — потому что авторы не ручаются за точную совместимость ввиду отсутствия подробного описания протокола CouchDB — из-за недостаточно подробной документации разработчикам Couchbase Lite пришлось отчасти реверсировать его. Протокол реализуется с помощью Sync Gateway — REST-сервиса репликации. Все клиенты, желающие синхронизировать между собой данные, должны работать с базой через этот сервис.
Не буду пересказывать процесс установки Couchbase, тем более что он отличается для разных платформ. Будем считать, что база данных уже установлена на localhost. Зайдём в интерфейс администрирования (по умолчанию http://localhost:8091/) и создадим бакет под названием «demo» — хранилище наших документов. Для этого откроем вкладку Data Buckets и щёлкнем кнопку Create New Data Bucket.

Введём имя бакета «demo» и ограничим его по квоте памяти в 100 мегабайт.

Если всё прошло хорошо, то в списке бакетов появится demo с зелёным кружком, символизирующим его активность.

Щёлкнув кнопку Documents, убедимся, что вновь созданный бакет пока пуст.

Установка и запуск сервиса Sync Gateway описаны в документации. В этой статье я приведу только конфигурационный файл sync-gateway-config.json, позволяющий повторить указанные в статье действия:
Запустив Sync Gateway с этим конфигурационным файлом, получим лог следующего содержания, свидетельствующий о том, что бакет с названием «demo» готов к использованию в качестве центрального хранилища для синхронизации данных:
Если теперь обновить список документов в бакете, то мы обнаружим там некоторые служебные документы Sync Gateway, идентификаторы которых начинаются на _sync.

Код консольного приложения доступен на GitHub вместе с кодом мобильного приложения и в большей степени предназначен для демонстрации и тестирования взаимодействия. Это небольшое приложение на Java, работающее с локальной базой данных Couchbase Lite опять же на платформе Java. Приложение умеет создавать в локальной базе данных документ с приложенным изображением (вложение image) и атрибутом даты/времени добавления (атрибут timestamp_added), а также запускать репликацию изменений в серверную базу данных Couchbase.
Мобильное приложение отображает уменьшенные превью картинок, которые были добавлены в консольном приложении и сохранены в базу. Процесс разработки мобильного приложения я опишу в этой статье более подробно. В качестве платформы использовалась iOS как имеющая наилучшую поддержку со стороны API Couchbase Lite. В качестве языка использовался Swift.
Для начала создадим простой проект типа Single View Application:

Для подключения к проекту библиотеки couchbase-lite-ios воспользуемся менеджером зависимостей CocoaPods. Установка CocoaPods описана здесь. Инициализируем CocoaPods в каталоге проекта:
Добавим в файл Podfile зависимость couchbase-lite-ios:
Установим нужные библиотеки:
Теперь повторно откроем проект уже как рабочее пространство (файл CouchbaseSyncDemo.xcworkspace). Добавим в него бриджинг-файл, чтобы библиотеки на Objective C, подключенные с помощью CocoaPods, можно было использовать в классах на Swift. Для этого добавим к проекту заголовочный файл с названием CouchbaseSyncDemo-Bridging-Header.h следующего содержания:
Укажем этот файл в Build Settings проекта:

Автоматически созданный класс ViewController унаследуем от UICollectionViewController:
Откроем файл Main.storyboard и заменим ViewController, созданный по умолчанию, на Collection View Controller, перетащив его из библиотеки объектов и перенаправив на него стрелочку Storyboard Entry Point. В качестве класса контроллера в секции Custom Class раздела Identity Inspector пропишем созданный по умолчанию ViewController. Также выберем Collection View Cell и в настройках Attribute Inspector пропишем ей идентификатор повторного использования «cell». На скриншоте представлен полученный результат.

Создадим класс CouchbaseService, который будет отвечать за работу с локальной базой данных Couchbase Lite, и реализуем его в виде синглтона:
В конструкторе класса откроем базу данных с названием demo. Запустим непрерывную входящую репликацию (pull) — если приложение запускается под эмулятором, и серверную базу данных мы развернули на той же машине, то в качестве адреса для репликации можно использовать localhost. Флаг continuous означает, что будет использоваться «непрерывная» репликация с использованием long polling. Также создадим представление images для извлечения списка картинок:
Представление в Couchbase — это индексированный и автоматически обновляющийся результат выполнения функций map и (опционально) reduce на всём массиве документов, хранящихся в бакете. В данном случае представление задаётся только своей map-функцией, которая для каждого документа возвращает в качестве ключа время его добавления в базу. По ключу в представлениях происходит сортировка результатов, так что картинки в результатах выполнения запросов к этому представлению всегда будут отсортированы по дате добавления. Параметр version — это версия представления, она должна изменяться каждый раз, когда мы меняем код представления. Смена версии позволяет нам сообщить Couchbase о том, что код функций map и reduce поменялся, и представление необходимо перестроить с нуля.
К представлениям в Couchbase можно выполнять запросы. Особый тип запросов — так называемые live-запросы, результаты которых представляют собой автоматически обновляющийся массив документов. Благодаря имеющемуся в Objective C и Swift механизму KVO, мы можем подписаться на изменение этого массива и обновлять интерфейс приложения при поступлении новых данных.
К сожалению, такой способ отслеживания изменений сигнализирует лишь о самом факте обновления результатов запроса, а не о конкретных добавленных или удалённых записях. Подобная информация позволила бы минимизировать обновление интерфейса — и такой механизм в Couchbase Lite тоже есть. Это подписка на событие kCBLDatabaseChangeNotification, сигнализирующее обо всех новых ревизиях, добавляющихся в базу данных. Но в данном примере я решил не рассматривать его, а использовать более простой механизм live-запросов.
Итак, добавим в CouchbaseService функцию для выполнения live-запроса к созданному ранее представлению images:
Версия Couchbase Lite для iOS отличается от других платформ тем, что в ней реализован механизм автоматического двустороннего маппинга документов и объектных моделей. Здесь задействуются динамические свойства языка Objective C, и с поправкой на Swift выглядит это примерно так:
Поле timestamp_added динамически привязывается к одноимённому полю в документе, а с помощью функции attachmentNamed: можно получить бинарные данные, приложенные к документу. Чтобы преобразовать документ в его объектную модель, мы можем воспользоваться конструктором ImageModel.
Теперь остаётся лишь подписать ViewController на обновление live-запроса и обработать это обновление, перерисовав коллекцию. В атрибуте images будем хранить список документов, преобразованных в объектные модели.
Функции контроллера из протокола UICollectionViewDataSource достаточно стандартны и не требуют пояснений, кроме того, что здесь мы используем идентификатор повторного использования «cell», заданный нами в storyboard для ячейки:
Теперь проверим, что получилось. Запустим консольное приложение. С помощью команды start начнём репликацию, а командой attach загрузим несколько изображений, на каждое из которых будет создан отдельный документ.
Данные реплицируются на мобильное устройство и тут же отобразятся в интерфейсе приложения:

В данной статье я продемонстрировал репликацию данных между серверной БД и мобильным устройством на основе Couchbase и Couchbase Lite для создания мобильного приложения, которое может полноценно работать офлайн. В следующих статьях я собираюсь поподробнее рассмотреть механизм хранения ревизий документов и протокол репликации Couchbase Lite, как он справляется с ситуациями обрыва связи, ухода приложения в фоновый режим и прочими «радостями» мобильной разработки.
Исходный код к статье на GitHub
База данных Couchbase
База данных Couchbase Lite
Сервер синхронизации Sync Gateway
Модель вычислений MapReduce
Установка Couchbase
Установка и запуск Sync Gateway
Установка CocoaPods
Мы — компания «Центр информационных технологий», создаем инфраструктурные решения и высокотехнологичные программные продукты, поддерживающие глобальные государственные инициативы в Российской Федерации и странах Евразийского экономического союза.
В этом посте мы поделимся с вами нашим опытом в области мобильной разработки приложений, ориентированных на работу с данными.

Couchbase и Couchbase Lite
При разработке мобильных приложений, ориентированных на работу с данными, мы часто сталкиваемся с необходимостью полноценной офлайновой работы приложения, а также синхронизации изменений в данных между мобильным приложением и бэкэндом, с которым также работают пользователи десктопных или веб-приложений.
Использование «облаков» для синхронизации данных далеко не всегда позволительно, особенно если дело касается заказчиков высокого уровня, которые не допускают такого решения из соображений безопасности и требуют развёртывания всех компонентов системы in-house. В этой статье я расскажу о нашем опыте решения этой задачи с помощью связки полноценной серверной базы данных Couchbase и «облегчённой» мобильной базы данных Couchbase Lite.
База данных Couchbase — документная распределённая NoSQL-база данных, обеспечивающая высокую производительность за счёт записи данных в первую очередь в оперативную память и уже потом (eventual persistence) на дисковое хранилище. Благодаря независимости и равноправию узлов с привязкой документа к конкретному узлу, Couchbase обеспечивает сильную целостность (strong consistency). Запросы к Couchbase строятся на основе индексированных представлений, реализующих модель вычислений MapReduce.
База данных Couchbase Lite — это легковесная версия Couchbase, предназначенная для использования в десктопных и мобильных приложениях и поддерживающая синхронизацию с серверной базой Couchbase. Реализации этой базы данных есть под iOS, Android, Java и .NET, так что её можно использовать не только в мобильных, но и в десктопных приложениях. Стоит упомянуть, что реализация Couchbase Lite под iOS на текущий момент обладает рядом преимуществ по сравнению с остальными платформами — например, там есть возможность полнотекстового поиска, а также средства для автоматического маппинга документов в объектные модели.
Для синхронизации Couchbase и Couchbase Lite используется протокол репликации, «совместимый» с протоколом CouchDB. Совместимый в кавычках — потому что авторы не ручаются за точную совместимость ввиду отсутствия подробного описания протокола CouchDB — из-за недостаточно подробной документации разработчикам Couchbase Lite пришлось отчасти реверсировать его. Протокол реализуется с помощью Sync Gateway — REST-сервиса репликации. Все клиенты, желающие синхронизировать между собой данные, должны работать с базой через этот сервис.
Установка и настройка серверной части
Настройка Couchbase
Не буду пересказывать процесс установки Couchbase, тем более что он отличается для разных платформ. Будем считать, что база данных уже установлена на localhost. Зайдём в интерфейс администрирования (по умолчанию http://localhost:8091/) и создадим бакет под названием «demo» — хранилище наших документов. Для этого откроем вкладку Data Buckets и щёлкнем кнопку Create New Data Bucket.

Введём имя бакета «demo» и ограничим его по квоте памяти в 100 мегабайт.

Если всё прошло хорошо, то в списке бакетов появится demo с зелёным кружком, символизирующим его активность.

Щёлкнув кнопку Documents, убедимся, что вновь созданный бакет пока пуст.

Настройка Sync Gateway
Установка и запуск сервиса Sync Gateway описаны в документации. В этой статье я приведу только конфигурационный файл sync-gateway-config.json, позволяющий повторить указанные в статье действия:
{
"interface":":4984",
"adminInterface":"0.0.0.0:4985",
"log": ["CRUD+", "REST+", "Changes+", "Attach+"],
"databases":{
"demo":{
"bucket":"demo",
"server":"http://localhost:8091",
"users": {
"GUEST": {"disabled": false, "admin_channels": ["*"]}
},
"sync":`function(doc) {channel(doc.channels);}`
}
}
}
Запустив Sync Gateway с этим конфигурационным файлом, получим лог следующего содержания, свидетельствующий о том, что бакет с названием «demo» готов к использованию в качестве центрального хранилища для синхронизации данных:
23:27:02.411961 Enabling logging: [CRUD+ REST+ Changes+ Attach+]
23:27:02.412547 ==== Couchbase Sync Gateway/1.0.3(81;fa9a6e7) ====
23:27:02.412559 Configured Go to use all 8 CPUs; setenv GOMAXPROCS to override this
23:27:02.412604 Opening db /demo as bucket "demo", pool "default", server <http://localhost:8091>
23:27:02.413160 Opening Couchbase database demo on <http://localhost:8091>
23:27:02.601456 Reset guest user to config
23:27:02.601467 Starting admin server on 0.0.0.0:4985
23:27:02.603461 Changes+: Notifying that "demo" changed (keys="{_sync:user:}") count=2
23:27:02.604248 Starting server on :4984 ...
Если теперь обновить список документов в бакете, то мы обнаружим там некоторые служебные документы Sync Gateway, идентификаторы которых начинаются на _sync.

Консольное приложение
Код консольного приложения доступен на GitHub вместе с кодом мобильного приложения и в большей степени предназначен для демонстрации и тестирования взаимодействия. Это небольшое приложение на Java, работающее с локальной базой данных Couchbase Lite опять же на платформе Java. Приложение умеет создавать в локальной базе данных документ с приложенным изображением (вложение image) и атрибутом даты/времени добавления (атрибут timestamp_added), а также запускать репликацию изменений в серверную базу данных Couchbase.
Мобильное приложение
Мобильное приложение отображает уменьшенные превью картинок, которые были добавлены в консольном приложении и сохранены в базу. Процесс разработки мобильного приложения я опишу в этой статье более подробно. В качестве платформы использовалась iOS как имеющая наилучшую поддержку со стороны API Couchbase Lite. В качестве языка использовался Swift.
Создание проекта и подключение зависимостей
Для начала создадим простой проект типа Single View Application:

Для подключения к проекту библиотеки couchbase-lite-ios воспользуемся менеджером зависимостей CocoaPods. Установка CocoaPods описана здесь. Инициализируем CocoaPods в каталоге проекта:
pod init
Добавим в файл Podfile зависимость couchbase-lite-ios:
target 'CouchbaseSyncDemo' do
pod 'couchbase-lite-ios', '~> 1.0'
end
Установим нужные библиотеки:
pod install
Теперь повторно откроем проект уже как рабочее пространство (файл CouchbaseSyncDemo.xcworkspace). Добавим в него бриджинг-файл, чтобы библиотеки на Objective C, подключенные с помощью CocoaPods, можно было использовать в классах на Swift. Для этого добавим к проекту заголовочный файл с названием CouchbaseSyncDemo-Bridging-Header.h следующего содержания:
#ifndef CouchbaseSyncDemo_CouchbaseSyncDemo_Bridging_Header_h
#define CouchbaseSyncDemo_CouchbaseSyncDemo_Bridging_Header_h
#import "CouchbaseLite/CouchbaseLite.h"
#endif
Укажем этот файл в Build Settings проекта:

Заготовка для интерфейса
Автоматически созданный класс ViewController унаследуем от UICollectionViewController:
class ViewController: UICollectionViewController {
Откроем файл Main.storyboard и заменим ViewController, созданный по умолчанию, на Collection View Controller, перетащив его из библиотеки объектов и перенаправив на него стрелочку Storyboard Entry Point. В качестве класса контроллера в секции Custom Class раздела Identity Inspector пропишем созданный по умолчанию ViewController. Также выберем Collection View Cell и в настройках Attribute Inspector пропишем ей идентификатор повторного использования «cell». На скриншоте представлен полученный результат.

Инициализация и запуск репликации
Создадим класс CouchbaseService, который будет отвечать за работу с локальной базой данных Couchbase Lite, и реализуем его в виде синглтона:
private let CouchbaseServiceInstance = CouchbaseService()
class CouchbaseService {
class var instance: CouchbaseService {
return CouchbaseServiceInstance
}
}
В конструкторе класса откроем базу данных с названием demo. Запустим непрерывную входящую репликацию (pull) — если приложение запускается под эмулятором, и серверную базу данных мы развернули на той же машине, то в качестве адреса для репликации можно использовать localhost. Флаг continuous означает, что будет использоваться «непрерывная» репликация с использованием long polling. Также создадим представление images для извлечения списка картинок:
private let pull: CBLReplication
private let database: CBLDatabase
private init() {
// создаём или открываем БД
database = CBLManager.sharedInstance().databaseNamed("demo", error: nil)
// создаём входящую репликацию
let syncGatewayUrl = NSURL(string: "http://localhost:4984/demo/")
pull = database.createPullReplication(syncGatewayUrl)
pull.continuous = true;
pull.start()
// создаём представление со всеми документами в базе
database.viewNamed("images").setMapBlock({(doc: [NSObject : AnyObject]!, emit: CBLMapEmitBlock!) -> Void in
emit(doc["timestamp_added"], nil)
}, version: "1")
}
Представления в Couchbase Lite
Представление в Couchbase — это индексированный и автоматически обновляющийся результат выполнения функций map и (опционально) reduce на всём массиве документов, хранящихся в бакете. В данном случае представление задаётся только своей map-функцией, которая для каждого документа возвращает в качестве ключа время его добавления в базу. По ключу в представлениях происходит сортировка результатов, так что картинки в результатах выполнения запросов к этому представлению всегда будут отсортированы по дате добавления. Параметр version — это версия представления, она должна изменяться каждый раз, когда мы меняем код представления. Смена версии позволяет нам сообщить Couchbase о том, что код функций map и reduce поменялся, и представление необходимо перестроить с нуля.
К представлениям в Couchbase можно выполнять запросы. Особый тип запросов — так называемые live-запросы, результаты которых представляют собой автоматически обновляющийся массив документов. Благодаря имеющемуся в Objective C и Swift механизму KVO, мы можем подписаться на изменение этого массива и обновлять интерфейс приложения при поступлении новых данных.
К сожалению, такой способ отслеживания изменений сигнализирует лишь о самом факте обновления результатов запроса, а не о конкретных добавленных или удалённых записях. Подобная информация позволила бы минимизировать обновление интерфейса — и такой механизм в Couchbase Lite тоже есть. Это подписка на событие kCBLDatabaseChangeNotification, сигнализирующее обо всех новых ревизиях, добавляющихся в базу данных. Но в данном примере я решил не рассматривать его, а использовать более простой механизм live-запросов.
Работа с данными
Итак, добавим в CouchbaseService функцию для выполнения live-запроса к созданному ранее представлению images:
func getImagesLiveQuery() -> CBLLiveQuery {
return database.viewNamed("images").createQuery().asLiveQuery()
}
Версия Couchbase Lite для iOS отличается от других платформ тем, что в ней реализован механизм автоматического двустороннего маппинга документов и объектных моделей. Здесь задействуются динамические свойства языка Objective C, и с поправкой на Swift выглядит это примерно так:
@objc
class ImageModel: CBLModel {
@NSManaged var timestamp_added: NSString
var imageInternal: UIImage?
var image: UIImage? {
if (imageInternal == nil) {
imageInternal = UIImage(data: self.attachmentNamed("image").content)
}
return imageInternal
}
}
Поле timestamp_added динамически привязывается к одноимённому полю в документе, а с помощью функции attachmentNamed: можно получить бинарные данные, приложенные к документу. Чтобы преобразовать документ в его объектную модель, мы можем воспользоваться конструктором ImageModel.
Привязка интерфейса к данным
Теперь остаётся лишь подписать ViewController на обновление live-запроса и обработать это обновление, перерисовав коллекцию. В атрибуте images будем хранить список документов, преобразованных в объектные модели.
private var images: [ImageModel] = []
private var query: CBLLiveQuery?
override func viewDidAppear(animated: Bool) {
query = CouchbaseService.instance.getImagesLiveQuery()
query!.addObserver(self, forKeyPath: "rows", options: nil, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if object as? NSObject == query {
images.removeAll()
var rows = query!.rows
while let row = rows.nextRow() {
images.append(ImageModel(forDocument: row.document))
}
collectionView?.reloadData()
}
}
Функции контроллера из протокола UICollectionViewDataSource достаточно стандартны и не требуют пояснений, кроме того, что здесь мы используем идентификатор повторного использования «cell», заданный нами в storyboard для ячейки:
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! UICollectionViewCell
cell.backgroundView = UIImageView(image:images[indexPath.item].image)
return cell
}
Запуск приложения
Теперь проверим, что получилось. Запустим консольное приложение. С помощью команды start начнём репликацию, а командой attach загрузим несколько изображений, на каждое из которых будет создан отдельный документ.
start
CBL started
апр 15, 2015 11:41:14 PM com.github.oxo42.stateless4j.StateMachine publicFire
INFO: Firing START
push event: PUSH replication event. Source: com.couchbase.lite.replicator.Replication@144c1e50 Transition: INITIAL -> RUNNING Total changes: 0 Completed changes: 0
апр 15, 2015 11:41:15 PM com.github.oxo42.stateless4j.StateMachine publicFire
push event: PUSH replication event. Source: com.couchbase.lite.replicator.Replication@144c1e50 Transition: RUNNING -> IDLE Total changes: 0 Completed changes: 0
INFO: Firing WAITING_FOR_CHANGES
attach http://upload.wikimedia.org/wikipedia/commons/4/41/Harry_Whittier_Frees_-_What%27s_Delaying_My_Dinner.jpg
Saved image with id = 8e357b3c-1c7f-4432-b91d-321dc1c9fd9d
push event: PUSH replication event. Source: com.couchbase.lite.replicator.Replication@144c1e50 Total changes: 1 Completed changes: 0
push event: PUSH replication event. Source: com.couchbase.lite.replicator.Replication@144c1e50 Total changes: 1 Completed changes: 1
Данные реплицируются на мобильное устройство и тут же отобразятся в интерфейсе приложения:

Заключение
В данной статье я продемонстрировал репликацию данных между серверной БД и мобильным устройством на основе Couchbase и Couchbase Lite для создания мобильного приложения, которое может полноценно работать офлайн. В следующих статьях я собираюсь поподробнее рассмотреть механизм хранения ревизий документов и протокол репликации Couchbase Lite, как он справляется с ситуациями обрыва связи, ухода приложения в фоновый режим и прочими «радостями» мобильной разработки.
Ссылки
Исходный код к статье на GitHub
База данных Couchbase
База данных Couchbase Lite
Сервер синхронизации Sync Gateway
Модель вычислений MapReduce
Установка Couchbase
Установка и запуск Sync Gateway
Установка CocoaPods