Приветствуем, Хаброжители!

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

В этом посте мы поделимся с вами нашим опытом в области мобильной разработки приложений, ориентированных на работу с данными.



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

Комментарии (13)


  1. kostyl
    06.05.2015 18:36

    Круто, лично мне, интересно пока только использоватение как standalone NoSQL хранилище.


  1. nepster
    06.05.2015 20:18

    чем лучше за Realm?


    1. forketyfork Автор
      06.05.2015 20:48
      +1

      Realm — это интересная замена Core Data, но там на текущий момент нет каких-либо средств репликации между мобильной и серверной базой данных. В документации есть примеры получения данных из REST-сервисов, но это не репликация. В нашем случае необходима была единая система синхронизации данных между мобильными и десктопными приложениями через централизованный серверсайд.


      1. nepster
        06.05.2015 21:50

        Понятно, спасибо :) Каждой задаче — свой инструментарий. Я о том, что если не нужно делать репликаций — то Realm проще использовать


  1. gurinderu
    07.05.2015 10:01

    Насколько я понял связь между couchbase server и couchbase lite осуществляется через Couchbase Sync Gateway путем какого-то rest api. Что происходит в момент синхронизации? Поднимается 1 connect на все команды или 1 connect на команду? Можно ли сделать все через 1 сокет?


    1. forketyfork Автор
      07.05.2015 10:30

      Да, репликация происходит через REST API. Там всё сделано не очень оптимально, и об этом я как раз собираюсь рассказать в следующей статье. На каждое обращение к REST-сервису создаётся отдельный NSURLConnection (т.е. даже не NSURLSession). Загнать всё в один сокет в такой схеме вряд ли получится.
      Для нормальной синхронизации, конечно, нужен нормальный транспортный механизм (не поверх REST), оптимизированный под мобильные устройства и учитывающий их специфику. К сожалению, подобные решения мне попадались только в проприетарном софте. Сейчас как раз ищу что-то подобное или думаю над написанием своего решения.


      1. gurinderu
        07.05.2015 16:01

        Понятно. В общем нужно писать свой API поверх для нормальной синхронизации.


  1. Makaveli
    12.05.2015 23:03

    ввиду отсутствия подробного описания протокола CouchDB — из-за недостаточно подробной документации разработчикам Couchbase Lite пришлось отчасти реверсировать его.

    Странно, я думал CouchDB — опенсорсный, потому они его и форкнули в своё время


    1. forketyfork Автор
      12.05.2015 23:14

      Он опенсорсный, но проблема была именно в плохой документации — даже при наличии исходников восстановить протокол до полной совместимости непросто. Вот что пишут авторы Couchbase Lite:

      Couchbase Lite’s replication protocol is compatible with Apache CouchDB. This interoperability is an important feature, but implementing it was challenging because much of CouchDB’s replication protocol is undocumented.


      1. Makaveli
        12.05.2015 23:32

        Спасибо, не знал.

        Завидую вам, я давно приглядываюсь к Couchbase Lite. Даже скачивал какие-то примеры, смотрел что да как, но так пока и не придумал для себя задачи, чтобы её заюзать в мобильных приложениях. Хотя в вебе с удовольствием пользуюсь CouchDB.

        С конфликтами не сталкивались? Когда ревизии в удалённой и локальной базе не совпадают. Или у вас только синхронизация без изменений данных на телефоне?


        1. forketyfork Автор
          12.05.2015 23:52

          Синхронизация в обе стороны, изменения на телефоне тоже есть, но конфликты пока исключаются за счёт разделения по ролевой модели (какие-то данные правятся на десктопе, какие-то на телефоне). Средства для разрешения конфликтов в Couchbase Lite тоже есть, так как любое изменение в документе это новая ревизия, и при конфликте обе ревизии сохраняются. Но сами конфликты должны разрешаться в клиентском коде — удалением или слиянием ревизий. Пока что пробовать этот механизм не приходилось.


  1. dzigoro
    13.05.2015 11:51
    +1

    На Конференции Мёбиус был доклад про Couchebase, можно скачать доклад тут


    1. forketyfork Автор
      13.05.2015 12:42

      Спасибо, интересный доклад. Не знал про ForestDB.