Room vs SQLDelight vs Realm для офлайн Android-приложений
Room vs SQLDelight vs Realm для офлайн Android-приложений: сравните владение схемой, обработку конфликтов синхронизации и боль миграций до того, как вы примете решение.

Почему этот выбор потом становится болезненным
Большинство команд выбирают локальную базу данных, когда приложение ещё маленькое. Там хранятся черновики, кэшированные записи, иногда очередь синхронизации — и всё кажется управляемым. Но через год та же база может содержать старые правки, частично синхронизированные элементы, удалённые записи и данные пользователей, которые не открывали приложение месяцами.
Именно тогда и начинаются настоящие проблемы. Офлайн-приложения держат локальные данные гораздо дольше, чем кажется, поэтому ранние решения живут на тысячах устройств. Поле, добавленное в спешке, может пережить два изменения серверной модели.
Обычные чтение и запись — редко самая сложная часть. Проблемы начинаются во время обновлений и синхронизации. Пользователь редактирует заказ в самолёте. Кто-то другой меняет тот же заказ на сервере. Теперь приложению нужно решить, что важнее, что слить вместе и что увидит пользователь.
Плохой выбор не слишком мешает в первый день. Он начинает мешать, когда правила продукта меняются каждые несколько недель. Одно поле status превращается в три. Soft delete превращается в архивирование и восстановление. Сервер начинает отклонять устаревшие обновления. В этот момент база данных уже не просто хранит данные. Она несёт в себе бизнес-правила, историю синхронизации и старые решения по обновлению.
Вот почему выбор между Room, SQLDelight и Realm — это не только вопрос вкуса разработчиков. Это вопрос границ. Где живёт схема? Где живут значения по умолчанию? Кто решает правила конфликтов? Кто владеет истиной, когда локальные и серверные данные расходятся?
Если команда не ответит на эти вопросы заранее, правила расползутся по аннотациям, классам моделей, файлам миграций, коду репозиториев и обработчикам синхронизации. Тогда небольшое изменение продукта затронет пять мест и начнёт казаться рискованным.
Простой пример хорошо это показывает. Допустим, приложение сначала хранило одно поле name для клиента, а потом разделило его на имя и фамилию. Звучит просто, пока не вспомнишь про старые версии приложения, несинхронизированные правки, частичные обновления с сервера и пользователей, которые вернулись после недель офлайна. Библиотека важна, но владение схемой важнее. Если решить это сначала, выбор инструмента становится понятнее.
Как каждый вариант работает со владением схемой
Владение схемой определяет, кто меняет форму данных приложения и где именно происходят эти изменения. Это звучит абстрактно, пока кто-то не спросит: «Где вообще определена эта таблица?»
В Room работа со схемой остаётся близко к Android-коду. Сущности, связи и DAO обычно живут в Kotlin внутри модуля приложения, которое их использует. Для Android-команд это выглядит привычно. Минус в том, что база данных может оказаться разбросанной по нескольким файлам. Одно поле живёт в entity, запрос — в DAO, а миграция — в raw SQL.
SQLDelight идёт в противоположную сторону. SQL-файлы сначала определяют таблицы и запросы, а уже потом под них генерируется Kotlin-код. Если нужно понять, как работает таблица, вы открываете SQL. Ограничения, join'ы, индексы и правила уникальности собраны в одном месте, поэтому владение схемой видно сразу.
Realm больше опирается на объектную модель. Вместо SQL-таблиц и запросов вы работаете с классами моделей и API Realm. На раннем этапе это может казаться очень простым, особенно если команда мыслит объектами приложения, а не правилами базы данных. Компромисс в том, что поведение хранилища менее явно, и некоторые правила сложнее быстро проверить.
Хорошая проверка простая. Если коллеге нужно найти правило уникальности, запрос для одного экрана или первый файл, который меняется при изменении схемы, насколько быстро он это сделает?
Room отвечает на такие вопросы в первую очередь через Android-код. SQLDelight отвечает через SQL. Realm отвечает через определения моделей и поведение, которое управляется его API.
Это различие важнее, чем большинство списков возможностей. Если вашей команде удобно, когда слой приложения владеет формой базы данных, Room кажется естественным. Если вам важно, чтобы база данных оставалась явной и читаемой, SQLDelight обычно проще для понимания. Если команда предпочитает объектное локальное хранилище и готова к меньшему контролю над деталями SQL, Realm всё ещё может подойти.
Где на самом деле живут правила конфликтов синхронизации
Конфликты появляются в тот момент, когда два устройства меняют одну и ту же запись до того, как хотя бы одно из них синхронизируется. Один пользователь правит имя клиента в самолёте, другой меняет номер телефона из офиса, и оба ожидают, что их версия сохранится. База данных хранит обе попытки, но она не решает, чему должно доверять приложение.
В Room такое решение обычно живёт в коде приложения и на backend. Room может хранить временные метки, номера версий и очередь ожидающих изменений, но встроенного мнения о слиянии у него нет. Реальные правила обычно находятся в слое репозитория, воркере синхронизации и серверных эндпоинтах.
SQLDelight работает похоже, но более явно. Вы пишете SQL, значит, вокруг него вы обычно пишете проверки конфликтов в Kotlin и на сервере. Такой контроль полезен, но он означает, что команда сама отвечает за весь набор правил и должна удерживать поведение клиента и backend в одном направлении.
Realm может приблизить обработку конфликтов к модели данных, если вы выстраиваете большую часть стека вокруг Realm. Это может казаться проще, потому что данные и синхронизация находятся ближе друг к другу. Компромисс очевиден: чем сильнее вы опираетесь на такую модель, тем больше ваши правила конфликтов следуют подходу Realm вместо того, чтобы оставаться простыми и переносимыми.
Перед запуском команда должна ответить на несколько очень простых вопросов. Какая сторона выигрывает, если одно и то же поле меняется дважды? Вы сливаете данные по записи или по полям? Что происходит, если удалённый элемент возвращается с офлайн-устройства? Сколько попыток повторного запроса вы допускаете, прежде чем остановиться и зафиксировать проблему? Какому номеру версии или временной метке вы доверяете?
Вот где команды часто неверно оценивают Room, SQLDelight и Realm. Они сравнивают синтаксис запросов, скорость или время настройки, а потом откладывают политику конфликтов на потом. А «потом» быстро становится дорогим.
Если вы выбираете «последняя запись побеждает», скажите это прямо и используйте везде. Если вы сливаете поля, запишите, какие поля можно объединять, а какие нельзя. Проверьте правило на двух офлайн-устройствах, медленном интернете и одном действии удаления. Такой короткий тест обычно учит большему, чем неделя аккуратных демо-данных.
Как выглядит работа с миграциями в реальных проектах
Свежие установки скрывают почти все проблемы базы данных. Боль начинается, когда версия 12 сталкивается с телефоном, на котором всё ещё лежат данные от версии 3, записи, синхронизированные лишь наполовину, и поля, о которых команда перестала думать несколько месяцев назад.
В Room работа с миграциями прямолинейна. Вы сами пишете шаги обновления: добавить колонку, перенести данные, разделить таблицу, заполнить значения по умолчанию. Это может казаться монотонным, но компромисс понятен. Команда точно видит, как старые данные становятся новыми, а баги обычно указывают на один файл миграции, а не теряются внутри библиотеки.
SQLDelight часто выглядит чище при ревью. Изменения схемы живут в SQL, поэтому их можно проверить без прыжков между аннотациями и сгенерированными моделями. Если кто-то спрашивает, почему изменилась колонка или зачем нужно значение по умолчанию, ответ часто лежит прямо в скрипте миграции.
Realm в начале часто ощущается лёгким. Вы меняете объект, повышаете версию и продолжаете собирать экраны. Сложности появляются позже, когда правила продукта уходят от объектной модели. Простое поле status превращается в status, sync_state, updated_at и, возможно, комментарий о конфликте от сервера. Тогда работа с миграциями становится неудобной, потому что форма, которая раньше казалась естественной в коде, больше не совпадает с тем, как ведёт себя приложение.
Такое может произойти быстро даже в небольшом офлайн-приложении. Сначала вы храните локальную заметку с title и body. Через шесть месяцев вам нужны soft delete, правила слияния и способ понять, пришло ли изменение с телефона или с сервера. Вот здесь и проявляется разница в повседневной работе. Room заставляет делать работу напрямую. SQLDelight делает изменение заметным. Realm может заставить сначала распутать ранние решения по модели, прежде чем двигаться дальше.
Для тестирования недостаточно свежих установок. Используйте старые базы с грязными данными: строки с null, записи, созданные до переименования поля, ожидающие элементы, которые так и не синхронизировались, и дубли, появившиеся из-за прошлых ошибок.
Если команда тестирует только чистую установку, боль от миграций дождётся дня релиза.
Простой способ выбрать
Большинство команд выбирают офлайн-базу для Android, глядя прежде всего на стиль API. Обычно это неправильная проверка. Для офлайн-работы лучше другой тест: заставьте каждый вариант пройти через неприятные части до того, как вы примете решение.
Используйте короткий spike вместо таблицы возможностей.
- Запишите все экраны, которые должны работать без сети. Будьте конкретны. «История заказов» — это не то же самое, что «редактировать черновик заказа с кэшированными ценами и вложениями».
- Выпишите три конфликтных случая, которые приложение должно решать при синхронизации. Например, два пользователя редактируют одно и то же название заметки, удалённая запись возвращается со старого устройства или цена меняется, пока воркер весь день остаётся офлайн.
- Измените схему на бумаге дважды. Добавьте одно поле, затем разделите одну таблицу или объект на два. Оцените, кто обновляет запросы, миграции, тесты и тестовые данные.
- Соберите один реальный экран со самым сложным запросом, а не с простым CRUD-экраном. Затем симулируйте одно обновление приложения, когда старые локальные данные всё ещё лежат на устройстве.
- Выберите инструмент, который делает эти шаги рутинными, даже немного скучными.
Последний пункт особенно важен. В выборе между Room, SQLDelight и Realm скучность — это хорошо. Вам нужен инструмент, который делает изменения схемы, правила конфликтов и тесты обновлений настолько понятными, чтобы уставший разработчик всё равно смог сделать всё правильно в пятницу после обеда.
Некоторые закономерности обычно видны быстро. Если ваш самый сложный экран зависит от join'ов, фильтров и SQL, который вы хотите видеть напрямую, SQLDelight часто заслуживает своего места. Если команда уже знает Room, а приложение близко к стандартным Android-паттернам, Room часто оказывается более безопасным выбором. Если ваше приложение хорошо подходит под объектную модель Realm и команде нравится такой стиль, Realm сначала может казаться очень быстрым, но миграции и синхронизацию нужно проверить особенно тщательно, прежде чем доверять инструменту.
Не выбирайте по первой демонстрации. Выбирайте по первому неудобному изменению. Именно там обычно и начинается счёт за поддержку.
Реалистичный пример
Представьте полевое сервисное приложение для команды по ремонту. Техник открывает заявку в подвале без связи, добавляет заметки, делает фотографии и записывает использованные детали. Приложение должно сохранить всё это на устройстве и синхронизировать позже, когда телефон снова получит связь.
Теперь добавим типичную путаницу. Один сотрудник обновляет заявку на месте, а другой меняет ту же заявку из офиса или с другого устройства. Позже компания решает отслеживать гарантийные данные и хранить полную историю статусов вместо одного текущего поля status. Вот здесь простой демо-сценарий начинает разваливаться.
В таком приложении владение схемой важнее, чем быстрая настройка.
В Room Android-разработчики обычно сначала двигаются быстро. Модель данных живёт рядом с Kotlin-кодом, и это кажется естественным, если команда приложения владеет большей частью продукта. Но когда два сотрудника редактируют одну и ту же заявку, Room не решает, как объединить конфликты. Команде всё равно приходится писать эти правила где-то ещё. Заметки должны сливаться построчно? Должен ли новый список деталей заменять старый? Должен ли статус добавляться в историю, а не перезаписывать старое значение?
SQLDelight здесь ощущается строже, и это помогает. Схема лежит в SQL-файлах, которые можно читать, не прыгая между аннотациями и сгенерированным кодом. Когда команда добавляет поля гарантии и таблицу status_history, изменение становится явным. Поэтому синхронизационные правила легче обсуждать, потому что все смотрят на одни и те же колонки и таблицы.
Realm в первой версии часто ощущается очень гладко. Сохранять объекты локально быстро, а офлайн-работа комфортна. Боль появляется позже, когда записям нужны аккуратные правила слияния, история изменений и предсказуемые миграции. В этот момент объектная модель может казаться менее ясной, чем схема, построенная от структуры данных.
Для такого приложения ясное владение схемой важнее, чем быстрая настройка. Если ошибки синхронизации могут потерять гарантийные данные или неправильный список деталей, выбирайте инструмент, который делает правила данных очевидными, когда приложение становится сложным.
Ошибки, которые делают команды
Большинство команд не выбирают офлайн-базу для Android после серьёзного испытания. Они берут ту, у которой самый короткий туториал, самый аккуратный демо-проект или API, который кажется знакомым в первый день. Это экономит несколько часов вначале и может стоить недель позже.
Это видно во многих обсуждениях Room, SQLDelight и Realm. Люди сравнивают синтаксис, время настройки и примеры, но пропускают более трудный вопрос: кто владеет схемой и кто утверждает изменения, когда приложение уже хранит реальные пользовательские данные?
Ещё одна частая ошибка — оставить правила конфликтов расплывчатыми до тех пор, пока пользователи не начнут терять правки. Два телефона обновляют одну заметку, или сервер присылает более старые данные, и команда начинает лихорадочно искать причину. Один разработчик патчит воркер синхронизации, другой добавляет проверки в репозитории, а кто-то третий чинит экран. В итоге в приложении появляется три разных ответа на один и тот же конфликт.
Ещё хуже, когда правила данных расползаются по UI-коду, репозиториям и заданиям синхронизации. Поле статуса меняется в одном месте, но не в другом. Удалённый элемент возвращается после синхронизации. Черновик выглядит сохранённым на устройстве, но исчезает после переподключения. Пользователям всё равно, какой слой ошибся. Они просто видят, что доверие сломано.
Тестирование миграций — ещё одно место, где команды обманывают сами себя. Они проверяют чистую установку, может быть, один путь обновления, и двигаются дальше. Реальные телефоны почти никогда не выглядят так. У пользователей есть старые строки, частичные данные, устаревший кэш, записи, созданные более ранними версиями приложения, и странные состояния, вызванные багами, о которых команда уже забыла.
Пустая база данных почти ничего не доказывает. Скопированная база с грязными данными говорит гораздо больше.
Команды ещё и слишком рано запирают себя в одну модель. Они проектируют под сегодняшний день, а через шесть месяцев им нужны локальные поля, soft delete, частичная синхронизация, общие записи или история изменений. Проблема не в том, что поменялась библиотека. Проблема в том, что изменилось приложение, а исходная модель оказалась слишком жёсткой.
Более безопасный подход достаточно прост:
- Решите, где будут жить правила схемы, прежде чем писать много кода приложения.
- Напишите правила конфликтов до первого релиза с синхронизацией.
- Тестируйте миграции на старых, грязных базах данных.
- Предположите, что модель данных изменится раньше, чем вы ожидаете.
Если команда пропускает эти шаги, выбор библиотеки начинает казаться куда важнее, чем есть на самом деле.
Краткая проверка перед тем, как принять решение
Прежде чем выбирать локальную базу данных, проведите несколько простых проверок. Они экономят больше времени, чем любая таблица с бенчмарками. В разговоре про Room, SQLDelight и Realm команды часто сначала сравнивают синтаксис, но синтаксис редко является тем, что начинает мешать через шесть месяцев.
Начните с читаемости. Передайте схему, модели и заметки по миграциям разработчику, который их не проектировал. Если этот человек не может примерно за час объяснить хранящиеся данные, связи, nullable-поля и поток обновления, настройка уже слишком непрозрачна для офлайн Android-базы.
Помогает короткий чек-лист:
- Попросите одного человека объяснить схему простыми словами после короткого просмотра.
- Попросите другого человека в одном абзаце описать обработку конфликтов синхронизации.
- Проверьте путь обновления хотя бы из двух старых сборок приложения, а не только из последнего релиза.
- Посчитайте, сколько кода приложения зависит от специфичных для базы API и моделей.
- Запишите, кто владеет изменениями схемы, правилами конфликтов и разрушительными действиями вроде hard delete.
Вторая проверка особенно важна. Если ваше правило конфликта нужно рисовать на доске двадцать минут, оно ещё не готово. «Сервер побеждает, кроме локальных черновиков» — это ясно. «Это зависит от состояния, источника, порядка повторных попыток и нескольких флагов» обычно заканчивается багами.
Тестирование обновлений — это момент, где красивые демо разваливаются. Установите версию приложения из нескольких месяцев назад, создайте данные, а потом сразу обновитесь до текущей сборки. Повторите то же самое с ещё более старой версии. Если сейчас это раздражает, после появления реальных пользователей со старыми установками будет только хуже.
Стоимость выхода тоже заслуживает честного взгляда. Если когда-нибудь вы уйдёте с Realm или с инструментов, ориентированных на SQL, сможете ли вы оставить доменные модели и слой репозитория почти без изменений? Если каждый экран напрямую импортирует типы базы данных, вы уже привязываете себя к инструменту.
Есть ещё одна проверка, которая находится за пределами Android-кода. Продукт, backend и Android должны одинаково отвечать на простой вопрос: кто решает форму данных и кто решает, что происходит при столкновении двух изменений? Если у команд разные ответы, миграции схемы будут болезненными независимо от выбранного инструмента.
Что делать дальше
Перестаньте спорить в абстракции. Сделайте небольшой spike с одним реальным экраном из вашего приложения, одним случаем синхронизации и одной миграцией. Используйте одни и те же тестовые данные и одно и то же бизнес-правило в каждом инструменте. Это скажет вам о Room, SQLDelight и Realm больше, чем ещё один круг мнений.
Выберите экран, на котором ошибка офлайн-данных будет заметна сразу. Экран заметок, черновик заказа или список задач подойдут. Потом специально создайте один конфликт: отредактируйте одну и ту же запись на двух устройствах, синхронизируйте оба варианта и решите, какое изменение побеждает.
Spike должен ответить на четыре простых вопроса. Кто в команде владеет изменениями схемы? Кто владеет правилами слияния, когда локальные данные и сервер расходятся? Сколько кода меняется ради одной небольшой миграции? Можете ли вы протестировать эту миграцию до того, как обновление попадёт к пользователям?
Запишите ответы, пока spike ещё свежий. Команды часто пропускают это, а потом спустя месяцы только один разработчик знает, как работает база данных. Это замедляет ревью, усложняет исправление багов и превращает миграции в угадывание.
С самого начала держите обработку конфликтов подальше от UI-кода. Экран должен собирать ввод и показывать состояние. Ваш слой синхронизации или данных должен решать, оставить ли последнее изменение, слить поля или отправить запись на ручную проверку. Если смешать эту логику с UI, на каждом новом экране будет повторяться та же проблема.
Простой пример хорошо показывает риск. Менеджер по продажам редактирует заметку о клиенте в самолёте, а поддержка меняет ту же заметку из офиса. Если ваше правило говорит «сервер всегда побеждает», приложение может стереть полезную локальную правку. Если правило говорит «побеждает самая свежая временная метка», рассинхронировка часов всё равно может всё сломать. Нужно выбрать правило, протестировать его и держать в одном месте.
Если вам нужен внешний взгляд перед тем, как принять решение, Oleg Sotnikov на oleg.is помогает стартапам и небольшим компаниям разобраться с владением схемой, рисками миграций и архитектурой приложения. Такой обзор особенно полезен до первого крупного релиза с синхронизацией, когда модель ещё легко менять.