company_banner

Сказ о том, как каскадное удаление в Realm долгий запуск победило

    Все пользователи считают быстрый запуск и отзывчивый UI в мобильных приложениях само собой разумеющимся. Если приложение запускается долго, пользователь начинает грустить и злиться. Запросто можно подпортить клиентский опыт или вовсе потерять пользователя ещё до того, как он начал пользоваться приложением.

    Однажды мы обнаружили, что приложение Додо Пицца запускается в среднем 3 секунды, а у некоторых «счастливчиков» 15-20 секунд.

    Под катом история с хеппи эндом: про рост базы данных Realm, утечку памяти, то, как мы копили вложенные объекты, а после взяли себя в руки и всё починили.





    Автор статьи: Максим Качинкин — Android-разработчик в Додо Пицце.



    Три секунды от клика на иконку приложения до onResume() первого активити — бесконечность. А у некоторых пользователей время запуска доходило до 15-20 секунд. Как такое вообще возможно?

    Очень краткое содержание для тех, кому некогда читать
    У нас бесконечно росла база данных Realm. Некоторые вложенные объекты не удалялись, а постоянно накапливались. Время запуска приложения постепенно увеличивалось. Потом мы это починили, и время запуска пришло к целевому — стало менее 1 секунды и больше не растёт. В статье анализ ситуации и два варианта решения — по-быстрому и по-нормальному.

    Поиск и анализ проблемы


    Сегодня любое мобильное приложение должно запускаться быстро и быть отзывчивым. Но дело не только в мобильном приложении. Пользовательский опыт взаимодействия с сервисом и компанией — это комплексная вещь. Например, в нашем случае скорость доставки — один из ключевых показателей для сервиса пиццы. Если доставка быстрая, то пицца будет горячей, и клиент, который хочет есть сейчас, не будет долго ждать. Для приложения, в свою очередь, важно создавать ощущение быстрого сервиса, ведь если приложение только 20 секунд запускается, то сколько придётся ждать пиццу?

    Сначала мы сами сталкивались с тем, что иногда приложение запускается пару-тройку секунд, а потом до нас стали долетать жалобы других коллег о том, что «долго». Но стабильно повторить эту ситуацию нам не удавалось.

    Долго — это сколько? Согласно Google-документации, если холодный старт приложения занимает менее 5 секунд, то это считается «как бы нормально». Android-приложение Додо Пиццы запускалось (согласно Firebase метрике _app_start) при холодном старте в среднем за 3 секунды — «Not great, not terrible», как говорится.

    Но потом стали появляться жалобы, что приложение запускается очень-очень-очень долго! Для начала мы решили измерить, что же такое «очень-очень-очень долго». И воспользовались для этого Firebase trace App start trace.



    Этот стандартный трейс измеряет время между моментом, когда пользователь открывает приложение, и моментом, когда выполнится onResume() первого активити. В Firebase Console эта метрика называется _app_start. Выяснилось что:

    • Время запуска у пользователей выше 95-го процентиля составляет почти 20 секунд (у некоторых и больше), несмотря на то, что медианное время холодного запуска менее 5 секунд.
    • Время запуска — величина не постоянная, а растущая со временем. Но иногда наблюдаются падения. Эту закономерность мы нашли, когда увеличили масштаб анализа до 90 дней.



    На ум пришло две мысли:

    1. Что-то утекает.
    2. Это «что-то» после релиза сбрасывается и потом утекает вновь.

    «Наверное, что-то с базой данных», — подумали мы и оказались правы. Во-первых, база данных используется у нас как кэш, при миграции мы её очищаем. Во-вторых, база данных загружается при старте приложения. Всё сходится.

    Что не так с базой данных Realm


    Мы стали проверять, как меняется содержимое базы со временем жизни приложения, от первой установки и далее в процессе активного использования. Посмотреть содержимое базы данных Realm можно через Stetho или более подробно и наглядно, открыв файл через Realm Studio. Чтобы посмотреть содержимое базы через ADB, копируем файл базы Realm:

    adb exec-out run-as ${PACKAGE_NAME} cat files/${DB_NAME}

    Посмотрев на содержимое базы в разное время, мы выяснили, что количество объектов определённого типа постоянно увеличивается.


    На картинке показан фрагмент Realm Studio для двух файлов: слева — база приложения спустя некоторое время после установки, справа — после активного использования. Видно, что количество объектов ImageEntity и MoneyType сильно выросло (на скриншоте показано количество объектов каждого типа).

    Связь роста базы данных с временем запуска


    Неконтролируемый рост базы данных — это очень плохо. Но как это влияет на время запуска приложения? Померить это достаточно просто через ActivityManager. Начиная с Android 4.4, logcat отображает лог со строкой Displayed и временем. Это время равно промежутку с момента запуска приложения до конца отрисовки активити. За это время происходят события:

    • Запуск процесса.
    • Инициализация объектов.
    • Создание и инициализация активити.
    • Создание лейаута.
    • Отрисовка приложения.

    Нам подходит. Если запустить ADB с флагами -S и -W, то можно получить расширенный вывод с временем запуска:

    adb shell am start -S -W ru.dodopizza.app/.MainActivity -c android.intent.category.LAUNCHER -a android.intent.action.MAIN

    Если сгрепать оттуда grep -i WaitTime время, можно автоматизировать сбор этой метрики и посмотреть наглядно на результаты. На графике ниже приведена зависимость времени запуска приложения от количества холодных запусков приложения.



    При этом был такой же характер зависимости размера и роста базы, которая выросла с 4 МБ до 15 МБ. Итого получается, что со временем (с ростом холодных запусков) росло и время запуска приложения и размер базы. У нас на руках появилась гипотеза. Теперь оставалось подтвердить зависимость. Поэтому мы решили убрать «утечки» и проверить, ускорит ли это запуск.

    Причины бесконечного роста базы данных


    Прежде чем убирать «утечки», стоит разобраться, почему они вообще появились. Для этого вспомним, что такое Realm.

    Realm — это нереляционная база данных. Она позволяет описывать связи между объектами похожим способом, которым описывают многие ORM реляционные базы данных на Android. При этом Realm сохраняет напрямую объекты в памяти с наименьшим количеством преобразований и маппингов. Это позволяет читать данные с диска очень быстро, что является сильной стороной Realm, за которую его любят.

    (В рамках данной статьи этого описания нам будет достаточно. Более подробно о Realm можно прочитать в крутой документации или в их академии).

    Многие разработчики привыкли работать в большей степени с реляционными базами данных (например, ORM-базами c SQL под капотом). И такие вещи как каскадное удаление данных часто кажутся само собой разумеющимся делом. Но не в Realm.

    Кстати говоря, фичу каскадного удаления просят сделать уже давно. Эту доработку и другую, связанную с ней, активно обсуждали. Было ощущение, что она скоро будет сделана. Но потом всё перевелось в введение сильных и слабых ссылок, которые тоже автоматом решили бы эту проблему. По этой задаче был довольно живой и активный пул-реквест, который пока из-за внутренних сложностей поставили на паузу.

    Утечка данных без каскадного удаления


    Как именно утекают данные, если надеяться на несуществующее каскадное удаление? Если у вас есть вложенные Realm-объекты, то их нужно обязательно удалять.
    Рассмотрим (почти) реальный пример. У нас есть объект CartItemEntity:

    @RealmClass
    class CartItemEntity(
     @PrimaryKey
     override var id: String? = null,
     ...
     var name: String = "",
     var description: String = "",
     var image: ImageEntity? = null,
     var category: String = MENU_CATEGORY_UNKNOWN_ID,
     var customizationEntity: CustomizationEntity? = null,
     var cartComboProducts: RealmList<CartProductEntity> = RealmList(),
     ...
    ) : RealmObject()

    У продукта в корзине есть разные поля, в том числе картинка ImageEntity, настроенные ингредиенты CustomizationEntity. Также продуктом в корзине может являтся комбо со своим набором продуктов RealmList (CartProductEntity). Все перечисленные поля являются Realm-объектами. Если мы вставим новый объект (copyToRealm() / copyToRealmOrUpdate()) с таким же id, то этот объект полностью перезапишется. Но все внутренние объекты (image, customizationEntity и cartComboProducts) потеряют связь с родительским и останутся в базе.

    Так как связь с ними потеряна, мы их больше не читаем и не удаляем (только если не обращаться к ним явно или не чистить всю «таблицу»). Мы это назвали «утечками памяти».

    Когда мы работаем с Realm, то должны явно проходить по всем элементам и явно все удалять перед такими операциями. Это можно сделать, например, вот так:

    val entity = realm.where(CartItemEntity::class.java).equalTo("id", id).findFirst()
    if (first != null) {
     deleteFromRealm(first.image)
     deleteFromRealm(first.customizationEntity)
     for(cartProductEntity in first.cartComboProducts) {
       deleteFromRealm(cartProductEntity)
     }
     first.deleteFromRealm()
    }
    // и потом уже сохраняем

    Если сделать так, то всё будет работать как надо. В данном примере мы предполагаем, что внутри image, customizationEntity и cartComboProducts нет других вложенных Realm-объектов, поэтому нет других вложенных циклов и удалений.

    Решение «по-быстрому»


    Первым делом мы решили подчистить самые быстрорастущие объекты и проверить результаты — решит ли это нашу изначальную проблему. Сначала было сделано наиболее простое и интуитивно-понятное решение, а именно: каждый объект должен быть ответственным за удаление за собой своих детей. Для этого ввели такой интерфейс, который возвращал список своих вложенных Realm-объектов:

    interface NestedEntityAware {
     fun getNestedEntities(): Collection<RealmObject?>
    }

    И реализовали его в наших Realm-объектах:

    @RealmClass
    class DataPizzeriaEntity(
     @PrimaryKey
     var id: String? = null,
     var name: String? = null,
     var coordinates: CoordinatesEntity? = null,
     var deliverySchedule: ScheduleEntity? = null,
     var restaurantSchedule: ScheduleEntity? = null,
     ...
    ) : RealmObject(), NestedEntityAware {
    
     override fun getNestedEntities(): Collection<RealmObject?> {
       return listOf(
           coordinates,
           deliverySchedule,
           restaurantSchedule
       )
     }
    }

    В getNestedEntities мы возвращаем всех детей плоским списком. А каждый дочерний объект также может реализовывать интерфейс NestedEntityAware, сообщая что у него есть внутренние Realm-объекты на удаление, например ScheduleEntity:

    @RealmClass
    class ScheduleEntity(
     var monday: DayOfWeekEntity? = null,
     var tuesday: DayOfWeekEntity? = null,
     var wednesday: DayOfWeekEntity? = null,
     var thursday: DayOfWeekEntity? = null,
     var friday: DayOfWeekEntity? = null,
     var saturday: DayOfWeekEntity? = null,
     var sunday: DayOfWeekEntity? = null
    ) : RealmObject(), NestedEntityAware {
    
     override fun getNestedEntities(): Collection<RealmObject?> {
       return listOf(
           monday, tuesday, wednesday, thursday, friday, saturday, sunday
       )
     }
    }

    И так далее вложенность объектов может повторяться.

    Затем пишем метод, который рекурсивно удаляет все вложенные объекты. Метод (сделанный в виде экстеншена) deleteAllNestedEntities получает все верхнеуровневые объекты и методом deleteNestedRecursively рекурсивно удаляет всё вложенное, используя интерфейс NestedEntityAware:

    fun <T> Realm.deleteAllNestedEntities(entities: Collection<T>,
     entityClass: Class<out RealmObject>,
     idMapper: (T) -> String,
     idFieldName : String = "id"
     ) {
    
     val existedObjects = where(entityClass)
         .`in`(idFieldName, entities.map(idMapper).toTypedArray())
         .findAll()
    
     deleteNestedRecursively(existedObjects)
    }
    
    private fun Realm.deleteNestedRecursively(entities: Collection<RealmObject?>) {
     for(entity in entities) {
       entity?.let { realmObject ->
         if (realmObject is NestedEntityAware) {
           deleteNestedRecursively((realmObject as NestedEntityAware).getNestedEntities())
         }
         realmObject.deleteFromRealm()
       }
     }
    }

    Мы проделали это с самыми быстрорастущими объектами и проверили, что получилось.



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

    Решение «по-нормальному»


    База хоть и стала расти медленнее, но все равно росла. Поэтому мы стали искать дальше. В нашем проекте очень активно используется кеширование данных в Realm. Поэтому писать для каждого объекта все вложенные объекты трудозатратно, плюс повышается риск ошибки, ведь можно забыть указать объекты при изменении кода.

    Хотелось сделать так, чтобы не использовать интерфейсы, а чтобы всё работало само.

    Когда мы хотим, чтобы что-то работало само, приходится использовать рефлексию. Для этого мы можем пройтись по каждому полю класса и проверить, является ли он Realm-объектом или списком объектов:

    RealmModel::class.java.isAssignableFrom(field.type)
    
    RealmList::class.java.isAssignableFrom(field.type)

    Если поле является RealmModel или RealmList, то сложим объект этого поля в список вложенных объектов. Всё точно так же, как мы делали выше, только тут оно будет делаться само. Сам метод каскадного удаления получается очень простым и выглядит так:

    fun <T : Any> Realm.cascadeDelete(entities: Collection<T?>) {
     if(entities.isEmpty()) {
       return
     }
    
     entities.filterNotNull().let { notNullEntities ->
       notNullEntities
           .filterRealmObject()
           .flatMap { realmObject -> getNestedRealmObjects(realmObject) }
           .also { realmObjects -> cascadeDelete(realmObjects) }
    
       notNullEntities
           .forEach { entity ->
             if((entity is RealmObject) && entity.isValid) {
               entity.deleteFromRealm()
             }
           }
     }
    }
    

    Экстеншн filterRealmObject отфильтровывает и пропускает только Realm-объекты. Метод getNestedRealmObjects через рефлексию находит все вложенные Realm-объекты и складывает их в линейный список. Далее рекурсивно делаем всё то же самое. При удалении нужно проверить объект на валидность isValid, потому что может быть такое, что разные родительские объекты могут иметь вложенные одинаковые. Этого лучше не допускать и просто использовать автогенерацию id при создании новых объектов.


    Полная реализация метода getNestedRealmObjects
    private fun getNestedRealmObjects(realmObject: RealmObject) : List<RealmObject> {
     val nestedObjects = mutableListOf<RealmObject>()
     val fields = realmObject.javaClass.superclass.declaredFields
    
    // Проверяем каждое поле, не является ли оно RealmModel или списком RealmList
     fields.forEach { field ->
       when {
         RealmModel::class.java.isAssignableFrom(field.type) -> {
           try {
             val child = getChildObjectByField(realmObject, field)
             child?.let {
               if (isInstanceOfRealmObject(it)) {
                 nestedObjects.add(child as RealmObject)
               }
             }
           } catch (e: Exception) { ... }
         }
    
         RealmList::class.java.isAssignableFrom(field.type) -> {
           try {
             val childList = getChildObjectByField(realmObject, field)
             childList?.let { list ->
               (list as RealmList<*>).forEach {
                 if (isInstanceOfRealmObject(it)) {
                   nestedObjects.add(it as RealmObject)
                 }
               }
             }
           } catch (e: Exception) { ... }
         }
       }
     }
    
     return nestedObjects
    }
    
    private fun getChildObjectByField(realmObject: RealmObject, field: Field): Any? {
     val methodName = "get${field.name.capitalize()}"
     val method = realmObject.javaClass.getMethod(methodName)
     return method.invoke(realmObject)
    }
    


    В итоге в нашем клиентском коде мы используем «каскадное удаление» при каждой операции изменения данных. Например, для операции вставки это выглядит вот так:

    override fun <T : Entity> insert(
     entityInformation: EntityInformation,
     entities: Collection<T>): Collection<T> = entities.apply {
     realmInstance.cascadeDelete(getManagedEntities(entityInformation, this))
     realmInstance.copyFromRealm(
         realmInstance
             .copyToRealmOrUpdate(this.map { entity -> entity as RealmModel }
     ))
    }

    Сначала метод getManagedEntities получает все добавляемые объекты, а потом метод cascadeDelete рекурсивно удаляет все собранные объекты перед записью новых. В итоге мы используем этот подход по всему приложению. Утечки памяти в Realm полностью исчезли. Проведя тот же замер зависимости времени запуска от количества холодных запусков приложения, мы видим результат.



    Зелёная линия показывает зависимость времени запуска приложения от количества холодных стартов при автоматическом каскадном удалении вложенных объектов.

    Результаты и выводы


    Постоянно растущая база данных Realm сильно замедляла запуск приложения. Мы выпустили обновление с собственным «каскадным удалением» вложенных объектов. И теперь отслеживаем и оцениваем, как наше решение повлияло на время запуска приложения через метрику _app_start.



    Для анализа берём промежуток времени 90 дней и видим: время запуска приложения, как медианное, так и то, что приходится на 95 процентиль пользователей, начало уменьшаться и больше не поднимается.



    Если посмотреть на семидневный график, то метрика _app_start полностью выглядит адекватной и составляет меньше 1 секунды.

    Отдельно стоит добавить, что по умолчанию Firebase шлёт уведомления, если медианное значение _app_start превышает 5 секунд. Однако, как мы видим, на это не стоит полагаться, а лучше зайти и проверить его явно.

    Особенность базы данных Realm заключается в том, что это нереляционная база данных. Несмотря на простое использование, схожесть работы с ORM-решениями и связывание объектов, у неё нет каскадного удаления.

    Если это не учитывать, то вложенные объекты будут накапливаться, «утекать». База данных будет расти постоянно, что в свою очередь скажется на замедлении работы или запуске приложения.

    Я поделился нашим опытом, как быстро сделать каскадное удаление объектов в Realm, которого пока нет из коробки, но о котором давно говорят и говорят. В нашем случае это сильно ускорило время запуска приложения.

    Несмотря на обсуждение скорого появления этой фичи, отсутствие каскадного удаления в Realm сделано by design. Если вы проектируете новое приложение, то учитывайте это. А если уже используете Realm — проверьте, нет ли у вас таких проблем.
    Dodo Engineering
    О том, как разработчики строят IT в Dodo

    Similar posts

    Comments 50

      0

      А зачем вам вообще база? Что вы там храните и для каких сценариев?)
      Просто на мой взгляд ваш главный сценарий — зашёл, выбрал, оплатил, закрыл.

        +1
        так очевидно же, судя по отрывкам кода, что там хранится например корзина, заказы, меню
        вероятно, это сделано как холодный кэш, чтобы постоянно не дергать бэкенд запросами
          +1
          Базу мы используем, как заметил evgeniymx, как кеш. Кешируем много разных объектов, для того чтобы быстры восстановить текущее состояние приложения. От корзины до меню.
            +1
            Но зачем? Без интернета это всё равно не будет работать, проще загрузить заново всё самое актуальное на данный момент.

            Ну и чего там можно такого накешировать, чтобы база загружалась СЕКУНДЫ?
              0
              Мы кешируем, чтобы сразу получать состояние приложения, если оно было убито. Пользователь сразу увидит запущенное приложение с контентом. Данные мы потом подгрузим тоже конечно же.

              Также мы кешируем, чтобы не делать лишних запросов на сервер. Например, взять что-то из закешированного меню.
                +2
                Часто бывает так, что сначала видишь какой-то контент, а потом он через мгновение обновляется до актуального. По моим личным ощущениям, лучше сначала ничего, а потом гарантированно актуальные данные, чем что-то потенциально неактуальное. Во втором случае приходится дополнительно напрягаться, чтобы понять – оно ещё обновляется, или это уже финальные данные? Это оно уже обновилось, или это всё ещё кеш, а настоящие данные так и не пришли?
                  0
                  Да, я тоже натыкаюсь на такие ситуации :) Бывает неприятно. Но это один из способов. Я думаю, нет одного правильного решения для всех приложений. Всё зависит от множества причин, в том числе на сколько часто обновляются данные, какие именно пользовательские сценарии должен решать кеш и т.д.
                    0
                    В приложении для заказа пиццы с 99% вероятностью нет ни одного крошечного кусочка данных, который был бы статичный, и который стоило бы закешировать. Всё может обновиться в любую секунду – меню, корзина, картинки, описание, адреса – да что угодно. Какой смысл это кешировать, если время актуальности этих данных околонулевое?
                      0
                      Какой смысл это кешировать, если время актуальности этих данных околонулевое?

                      Кажется, у вас неправильное представление об условиях работы мобильных приложений. Допустим, заходит человек в метро на пути домой и хочет заказать пиццу. Он открывает приложение, оно подгружает актуальные данные. Далее, пока пользователь едет между станциями, он может бродить по приложению, выбирать, что он хочет. Также может отвлечься на другое приложение. И если на этом этапе приложение выгрузится с памяти (как пример причины потери данных из in-memory кэша) и человек не сможет дальше делать выбор до следующего доступа к сети, он может просто плюнуть и зайти в супермаркет по дороге, вместо того, чтобы по второму кругу искать нужные продукты в приложении, когда оно снова соизволит загрузить данные. Вряд ли за эти полчаса меню изменится настолько, что пользователю уже не сделают пиццу, которую он выбрал из закэшированных.
                      И речь здесь не только о данном приложении, или о любом приложении для заказа еды, а в принципе о мобильных приложениях, которые немного думают о пользователях.
                      agent10 это тоже касается.

                        0

                        Есть и другие примеры. Badoo. Кол-во пользователей многократно больше. Можете поискать инфу, они её тут и писали на Хабре, что почти ничего не кэшируют вообще в приложении.


                        И вы и мы немного о разном. Мы не спрашиваем о сохранении состояния в целом(что в целом важно). Мы говорим о наличии целой базы данных для этого. В которую как мы видим сохраняется чуть ли не всё вообще)

                          0

                          Бизнес-требования и сценарии использования у Badoo и приложения для заказа пиццы разные, как по мне. Тот же Badoo — станцию метро проехал, и вот уже список людей рядом серьезно изменился. Хотя, честно говоря, такого рода приложениями пользоваться не приходилось, поэтому утверждать ничего не берусь. Но то же меню в Додо вряд ли обновляется каждые 10 минут.


                          Мы не спрашиваем о сохранении состояния в целом(что в целом важно).
                          Мы говорим о наличии целой базы данных для этого.

                          Вот не вижу проблем, судя по скриншотам структуры базы и фрагментам кода, ничего криминального не кэшируется. Особых альтернатив и нет. Не в файлы, право, писать же, если нужны минимальные query по кэшу.
                          Что-то типа redux-persist могло бы быть интересным вариантом для простых кейсов, но держать в памяти весь кэш тоже не всегда адекватно. В ощутимом количестве случаев база как single source of truth только упрощает жизнь. Другое дело, что Realm — это своеобразная штука, которую надо уметь готовить. И как видно, ребята из Додо изначально это не совсем умели.
                          Мне вот интересно, а как вы данные кэшируете в своих приложениях?

                            0

                            Не соглашусь) Потерять "потенциального" партнёра из-за того, что-то там не закэшировалось может быть значительно хуже сорвавшейся пиццы))


                            Опять же, статья изначально вышла из того, что приложение стартовало до 15-20 секунд из-за БД. Сколько клиентов/заказов они потеряли при этом за всё время существования проблемы? Судя по графикам из статьи — сопоставимо больше, чем происходят редкие случаи system-kill приложения во время заказа в фоне…
                            Мой посыл в том, что важнее закрыть критичные для бизнеса вещи, но простыми и безопасными средствами, чем неумело использовать танк сразу для всего:)

                              0
                              Потерять "потенциального" партнёра из-за того, что-то там не закэшировалось может быть значительно хуже сорвавшейся пиццы

                              Не знаю, к сожалению или к счастью, но подобные приложения вряд ли об этом очень переживают.


                              неумело использовать танк сразу для всего:)

                              Ну, тут как бы статья вся вышла о том, как люди не разобрались в матчасти изначально. Realm не умеет апдейтить вложенные объекты, если у них нет primary key, issue об этом уже много лет. Что, в принципе, звучит логично, потому что непонятно что делать с объектами без primary key и как понять, где ещё они используются. Не знаю, насколько просто это понять из документации, но мой посыл в том, что проблемы с Realm, возникшие в ключе использования базы как кэша, не дискредитируют использование (любой) базы в таком виде как таковое.


                              происходят редкие случаи system-kill приложения во время заказа в фоне…

                              Эту ситуацию я привёл как пример, могут быть и другие причины, зачем кэш

              0
              Просто уберите все эти ваши метрики, х**трики, кеши и базы – и всё начнёт летать
                0
                Дело в том, что у нас кеши, как таковые, не тормозят нам что-то. Проблема была в том, что мы неправильно их использовали. В статье я описал как причину нашей проблемы, так её решение. Вдруг у кого есть будут похожие проблемы.

                Сейчас всё летает :)
                0

                Как верно заметили ниже — "но зачем?") При этом в одном из предыдущих постов вы указали, что не делает поддержку планшетов "ибо нет необходимости", но зато делаете кучу работы с базой данных и сами решаете проблемы с ней?) Overengineering в Dodo?:)

              +1
              @RealmClass
              class DataPizzeriaEntity(
               @PrimaryKey
               var id: String? = null,
               var name: String? = null,
               var coordinates: CoordinatesEntity? = null,
               var deliverySchedule: ScheduleEntity? = null,
               var restaurantSchedule: ScheduleEntity? = null,
               ...
              ) : RealmObject(), NestedEntityAware {
              

              Почему всё optional? Какая польза от объекта, у которого всё null?
                0
                Это не optional, это nullable.
                Какая именно «польза» тут я не знаю как ответить. Такое энтити. Например, касательно поле id — оно автогенерируется позднее, а остальные поля технически здесь оказались nullable.

                Но в целом здесь речь не про это, а про вложенные объекты и интерфейс NestedEntityAware.
                +1
                … раз у вас в «кеше» появилась 61 тысяча ImageEntity, и 187 тысяч (!!) других Entity – не напрашивается ли мысль, что их надо не «подчищать», а просто перестать кешировать? Они явно не переиспользуются.
                  0
                  Да, всё правильно, я согласен. Они не переиспользовались. Потому что они «утекли». Мы как бы потеряли к ним доступ. Они есть в базе, но мы к ним не обращались. Родительских элементов уже не было, а по id мы уже не могли.
                  Но мы решили не отказываться от идеи кеширования как таковой, а просто сделать, чтобы это всё работало правильно.
                    0
                    Я делаю вывод, что данные, которые вы пишете – имеют очень маленький срок жизни, и или часто обновляются, или же крайне редко (или никогда) перепрочитываются. К слову о кеше – есть ли такой сценарий, при котором вы только берёте из кеша, и не делаете запроса за свежими данными? Если нет (т.е. вы всё равно всегда запрашиваете данные с сервера), то имхо такой кеш совершенно бесполезен и даже вреден.
                      0
                      Да, есть сценарии, когда берем из кеша, если он еще валидный.
                        0
                        Можно пример сценария, и пример определения валидности кеша?
                          0
                          Примеры есть разные. Есть какие-то данные, которые кешируется для конкретной страны. Есть, которые кешируются по времени. Есть, которые кешируются по городу.

                          Но это не так важно в вопросе «нужно ли нам кешировать всё». Потому что даже быстро изменяющиеся данные мы тоже хотим брать из кеша при старте приложения.
                            0
                            даже быстро изменяющиеся данные мы тоже хотим брать из кеша при старте приложения

                            Я пытаюсь понять – чтобы что? Чтобы не было пустого экрана? Это проблема? По-моему это не проблема.
                              0
                              Я думаю, что здесь нет одного простого ответа. Здесь можно рассматривать с продуктовой точки зрения, на сколько это проблема. Можно рассматривать с разных технических точек зрения, например подхода Single source of truth, или оптимизации количества запросов на API, или еще что-то. И будут как свои плюсы, так и свои издержки (одна из таких издержек представлена в этой статье).

                              Но это немного другая тема, нежели о чем данная статья. На тему «а нужна ли кеш база данных в мобильном приложении», наверное, можно написать не одну, а ряд статей.
                                +1
                                Можно для начала написать ответ на конкретный вопрос о частном случае. :) Хоть с продуктовой точки зрения, хоть с любой другой. И я не очень понял, о каких издержках говорилось в статье.

                                Я вижу, что проделана какая-то работа по реализации кеша, по интеграции и поддержке стороннего решения (Realm), по решению проблем с чужой библиотекой, – и всё это выглядит зря потраченными усилиями, потому что не имея никакого кеша можно было уничтожить целый класс проблем, и в то же время не потерять в UX, а возможно даже улучшить его.

                                Отсюда вопрос: что за проблема решалась внедрением кеша, ради чего были испытаны все эти страдания?
                                  0
                                  В нашем частном случае было важно как запускать быстро приложение с закешированными данными, так и не делать лишних запросов на API. Это по совокупности перевесило чашу весов того, что это потребует дополнительных ресурсов.

                                  Издержки — это отсутствие удобного каскадного удаления. На которое, да, нам пришлось потратить усилия и время.

                                  Зря они потрачены или нет? На мой взгляд, на этот вопрос нет объективного ответа. Мы не можем померить, как вел бы себя наш продукт на наших масштабах с или без базы. Какие бы были показатели, и на сколько сильно они бы повлияли на пользовательский опыт? У меня нет ответа на этот вопрос.
                                    0
                                    Я посмотрел приложение для iOS. Я подозреваю, что основные видимые данные, которые надо загружать с сервера при запуске – это меню (и может быть промоакции). Хоть меню на первый взгляд и выглядит довольно статическим, но подозреваю, что большинство пользователей не заказывает пиццу каждый день, и между их сессиями меню успевает измениться.

                                    Почему бы просто не назначить cache-control на запрос меню, и не позволить системе самой разобраться с кешированием этого ответа? И вместо использования Realm, их модели данных, сабклассов их классов просто не парсить каждый раз этот ответ от сервера (закешированный системой) в свои маленькие объектики?
                                      0
                                      То же самое и с картинками – если у них не меняются url, и если сервер отдаёт правильные заголовки про кеширование – система (по крайней мене iOS) должна сама замечательно со всем этим справиться.
                  0
                  оффтоп об iOS приложении Додо Пиццы: кому в голову пришла гениальная идея локализации приложения на лету в зависимости от выбранной страны внутри приложения? Т.е. приехав в Румынию я должен заказывать пиццу со словарём? Сделать локализацию на лету это довольно нетривиальная задача, но сама эта идея – ошибка.
                    0

                    Таких кейсов пока очень мало, а меню пока только на языке своей страны. Переделаем когда это станет значимым.


                    Главная проблема была такой: приложение поддерживает n языков, потому что работает в n стран. Чтобы в приложении не было двух разных языков у интерфейса меню для каждой страны меню надо тоже заводить для n языков? Или только на родном и английском? Тогда в приложении тоже может быть только родной и английский? Кароч вопросы и сложность, а ценности этому пока нет.


                    Чисто на платформе язык умеет менять динамически, остальное лишь вопрос времени и запроса от пользователей.

                      0
                      Какой язык в системе (на каком языке работает всё остальное на телефоне) – такой и использовать.
                        0

                        Большие расходы на поддержку меню всех стран на всех языках.

                          +1
                          У вас большие расходы на штат разработчиков, уж перевести 1000 строчек с названиями пиццы сможет любая студентка.
                            +3
                            Зачем Додо Пицце 250 разработчиков?

                            О каких расходах на поддержку меню на 5 языках (русский, английский, эстонский, литовский, румынский) идёт речь?
                          0
                          У вас в российском меню ~150 позиций, а в остальных – раза в два меньше, причём ассортимент сильно пересекается. И скорее всего большая часть уже и так переведена на английский.

                          Можно было бы сделать такой приоритет: Язык системы – язык региона – base (английский).
                            +2
                            Всё можно сделать. И мы когда-нибудь обязательно сделаем.

                            На этот счет, я буду рад поделиться очень интересной мыслью от Эрика Бернардсона (бывший СТО Спотифая), именно вот этой мыслью. Грубо говоря она говорит о том, что какую-либо недоработку или нереализованную фичу стоит в первую очередь рассматривать с точки зрения оценки упущенной выгоды. Если говорить проще, то если какой-то функционал еще где-то не сделали, то это не значит что это не важно. Наиболее вероятно это значит, что до этого делали более приоритетный функционал. Как-то так.

                              0
                              Да, но вы ведь потратили время на то, что по-человечески сделать нельзя: iOS не поддерживает смену языков в приложении на лету.
                                0

                                Мы потратили на это не так много, может часов 40-60 на обе платформы. Это ничто в сравнении с расходами, когда для добавления каждого нового продукта надо дернуть партнеров из разных стран, поддерживать многоязыковое меню и писать фолбеки не те случаи когда меню еще не переведено.


                                Ну и технически сделано довольно ок, подменяем бандл в рантайме.

                        0
                        А зачем вообще было решено выбрать Realm? У него же много проблем, почему не старый добрый sqlite с какой-нибудь надстройкой типо Room'a?
                          0
                          Realm имеет ряд преимуществ. Он супер быстро делает операции чтения. Он очень прост в использовании, нежели голый SQLite. И несмотря на его недостатки, в целом Realm — это рабочий и стабильный инструмент.

                          Room, на момент проектирования приложения, был еще довольно новым инструментом.

                          Если бы мы проектировали приложение сейчас, то, возможно, выбрали бы другой инструмент.
                            0
                            Операции чтения быстрые насколько мне известно за счет того, что он не сразу на диск пишет, а в in memory кэш сначала складывает, если не ошибаюсь. А в чем простота по сравнению с sqlite? Мне лично наоборот показался менее удобным, более сложным и неочевидным. Мой личный опыт, к сожалению, обратный. Скорее наоборот было сильное желание выкинуть его, но т.к. проект достался по наследству, то это было не так то просто((

                            1) Конкретно мне кажется оч странным то, что в каком потоке создали объект, в том с ним и работайте)))
                            2) Ужасный Realm studio, который многим уступает клиентам для sql. Элементарно не сделать выборку из базы c разными условиями, как это можно с sql.
                            3) Еще и тянуть за собой несколько мегабайт нативных .sошек

                            >Room, на момент проектирования приложения, был еще довольно новым инструментом. — Ну, понятное дело, но кроме него много альтернатив)))

                            Конечно, я не пытаюсь разубедить что это плохой инструмент. Если вам подошло, то я только рад за Вас)
                            0

                            Ладно бы это все было ради какого-то развесистого офлайн-режима как в Фейсбук-мессенджере, но тут все явно можно на уровне json-ов закешировать. Какие преимущества базы в вашем случае?


                            Второй вопрос, почему рост кеша настолько замедляет старт приложения. Вы его в память весь при старте загружаете до показа интерфейса? Тогда ещё раз встаёт вопрос о необходимости базы.


                            Если нет, может каких-то индексов не хватает. 160 тысяч строк в таблице базы не звучит как что-то огромное, откуда 25 секунд нужно искать сущности для показа стартового интерфейса.

                              0
                              Преимущества работают в комплексе для базы данных. Нет одного. Это и быстрое восстановление, и меньше запросов в бэк. Притом в базе данные хранятся не единым куском. Если есть ссылка на товар из меню, то идем и берем из меню. Или например, схема данных может меняться в разных версиях, поддерживать версионность на уровне json'ов совсем не удобно.

                              Мы в память загружаем при старте не так много. Меню и еще кое каки вещи. И это довольно компактный объем данных. Но когда база распухшая на много сотен тысяч строк, то запросы в нее идут медленнее. Особенно когда надо запрашивать всё меню, а не по индексам что-то.
                              0
                              Я тоже с такой проблемой сталкивался. Решил путем присваивания всем вложенным объектам id primary key, базирующемся на id родителя.
                                0
                                Интересно. А расскажите подробнее. Т.е. вы придумали способ, как однозначно узнать id дочерних элементов на основе родительских? И поэтому обращались впредь по тем id? Ну и вы руками удаляли всё перед вставкой, да?
                                  0
                                  Например у меня есть модель родитель Job, у нее есть id, генерируемый сервером. У этой модели есть список вложенных объектов, например RealmList и RealmList. Сами User и Employer имели id, но так так сама модель job имела списки этих моделей, то при сохранении ее получалось то же что и у Вас — дублирование массы одних и тех же объектов в БД. Я просто брал и присваивал этим спискам id, что то типа job.users.id= «users_${job.id}. И тогда все лежало на своих местах и ничего не приходилось удалять даже. Возможно ситуация не совсем такая как у Вас. Просто не пойму, если в Вашем примере вложенные объекты имеют свои id(Primary Key), почему связь теряется. Они по идее должны так же перезаписыватся(обновлятся).
                                    0
                                    Я понял.
                                    У нас речь о вложенных объектах без Primary Key. У нас много вложенных объектов и для них сервер не присылает id. Для них Primary Key, который уникальный и не особо имеет смысл.
                                    Если посмотрите, как пример в статье, объект с расписанием ScheduleEntity. У него нет Primary Key. У него вложенные объекты, у которых тоже нет Primary Key.
                                0

                                Почему-то вспоминаются современные подкасты:
                                — Realm не удаляет каскадно зависимости автоматом;
                                — О, есть о чем поговорить 1.5 часа


                                <joke-off-cut/>

                                Тем не менее, статья хорошая. Олеся — спасибо!

                                Only users with full accounts can post comments. Log in, please.