НейроАгент

Эффективный подсчет связей в CoreData для SwiftUI

Узнайте самые эффективные методы отображения количества связей в CoreData в SwiftUI. Узнайте, почему производные атрибуты превосходят запросы выборки и подсчет связей для оптимальной производительности.

Какой самый эффективный способ отображения информации о отношениях Core Data в SwiftUI?

У меня есть сущность Core Data Topic, которая имеет отношение “один-ко-многим” к Question (questions с обратным отношением topic). Каждый Question содержит свойство previousAttempt.

Я работаю над представлением SwiftUI, которое отображает список доступных тем. Верхний слой - это TopicsView, который содержит @FetchRequest для получения всех тем. Затем следует следующий ForEach:

swift
ForEach(topics, id: \.objectID) {topic in
    TopicCellView(topic: topic)
}

Внутри TopicCellView я хочу отображать заголовок темы и количество вопросов, у которых previousAttempt больше 0 (то есть тех, которые были правильно отвечены хотя бы один раз).

Какой правильный/эффективный способ сделать это? По моему мнению, есть следующие варианты:

1) Использование @FetchRequest

swift
struct TopicCellView: View {
    @ObservedObject private var topic: Topic
    @FetchRequest private var correctQuestions: FetchedResults<Question>
    
    init(topic: Topic) {
        self.topic = topic
        _correctQuestions = FetchRequest<Question>(sortDescriptors: [], predicate: NSPredicate(format: "topic == %@ AND previousAttempt > 0", argumentArray: [topic]))
    }

    var body: some View {
        Text(correctQuestions.count)
    }
}

2) Подсчет правильных вопросов в init с использованием отношения

swift
struct TopicCellView: View {
    @ObservedObject private var topic: Topic
    private let correctQuestionsCount: Int
    
    init(topic: Topic) {
        self.topic = topic
        correctQuestionsCount = topic.questions.count { $0.previousAttempt > 0 }
    }

    var body: some View {
        Text(correctQuestionsCount)
    }
}

3) Подсчет правильных вопросов в init с использованием запроса выборки

swift
struct TopicCellView: View {
    @ObservedObject private var topic: Topic
    private let correctQuestionsCount: Int
    
    init(topic: Topic) {
        self.topic = topic
        correctQuestionsCount = // выполнить запрос выборки Core Data здесь
    }

    var body: some View {
        Text(correctQuestionsCount)
    }
}

4) Добавление correctQuestionsCount как свойства к topic

Это чисто концептуальный вариант без кода.

Причина этого вопроса в том, что я пытаюсь оптимизировать свои представления SwiftUI и вижу, что при использовании Core Data у меня часто бывают долгие обновления тела представления. Я пробовал решения 1-3 выше, и все они иногда вызывают долгие обновления тела представления. Возможно, это неизбежно, но я все равно хотел бы понять, какой самый эффективный способ получения и отображения такой информации.

Я предполагаю, что вариант №4 будет наиболее производительным, но он точно склонен к ошибкам, поэтому я хотел бы его избегать, если это совершенно не необходимо.

НейроАгент

Самый эффективный способ отображения информации о связях CoreData в SwiftUI — использование производных атрибутов в модели Core Data (Вариант 4), в сочетании с правильными техниками предварительной выборки. Этот подход минимизирует запросы к базе данных и использование памяти, обеспечивая обновление в реальном времени при изменении исходных данных.


Содержание


Основные проблемы производительности

При работе со связями CoreData в SwiftUI основные проблемы производительности связаны с тем, как Core Data управляет объектными связями и как SwiftUI запускает обновления представлений.

Подход с @FetchRequest (Вариант 1) создает отдельные запросы выборки для каждого TopicCellView, что приводит к:

  • Множественным запросам к базе данных для каждой темы
  • Частым обновлениям представлений при изменении данных
  • Накоплению памяти из-за поддержания отдельных контекстов выборки

Аналогично, подсчет связей в коде Swift (Вариант 2) заставляет Core Data загружать все связанные объекты в память перед их фильтрацией и подсчетом, что становится все менее эффективным по мере роста размера связей.

Как объясняется на SwiftLee, “вызов метода count для свойства связи позволит получить количество объектов в этой связи для одной записи”, но этот подход работает на уровне Swift, а не использует нативные возможности базы данных для подсчета.


Производные атрибуты: Наиболее эффективное решение

Производные атрибуты (Вариант 4) обеспечивают наиболее эффективное решение, выполняя подсчет на уровне базы данных. Этот подход:

  1. Использует подсчет на уровне SQL вместо загрузки и фильтрации объектов в памяти
  2. Сохраняет количество как часть модели данных для немедленного доступа
  3. Автоматически обновляется при добавлении, удалении или изменении связанных объектов
  4. Снижает накладные расходы на память, избегая ненужного формирования объектов

Согласно SwiftLee, “в этом случае мы заполняем articles.@count, что в основном означает, что мы хотим использовать связь articles с помощью агрегатной функции @count. Сохранение конфигурации модели сгенерирует обновленные классы сущностей, и вы сможете использовать новый атрибут.”

Шаги реализации:

  1. Добавьте новый атрибут в сущность Topic:

    • Имя: correctQuestionsCount
    • Тип: Integer 16
    • Отметьте “Optional” при необходимости
  2. Настройте его как производный атрибут:

    • Установите выражение для использования агрегатной функции count
    • Укажите путь связи к вашей связи questions
    • Добавьте фильтрацию для previousAttempt > 0
  3. Обновите ваш TopicCellView:

swift
struct TopicCellView: View {
    @ObservedObject private var topic: Topic
    
    var body: some View {
        Text("\(topic.correctQuestionsCount)")
    }
}

Как отмечает fatbobman, “по сравнению с прямым вызовом свойства .count связи, использование производного атрибута для подсчета обычно более эффективно. Это потому, что производные атрибуты используют другой механизм подсчета — они вычисляют и сохраняют количество…”


Альтернативные подходы и их ограничения

Вариант 1: @FetchRequest

Хотя этот подход функционален, он создает множественные запросы к базе данных и может привести к проблемам производительности в сложных приложениях. Каждый TopicCellView поддерживает свой собственный запрос выборки, вызывая ненужную нагрузку на базу данных.

Вариант 2: Подсчет связей в Swift

Этот метод загружает все связанные объекты в память перед фильтрацией, что становится все менее эффективным по мере роста размера связей. Результаты поиска последовательно показывают, что операции на уровне базы данных превосходят обработку в памяти.

Вариант 3: Ручной запрос выборки в init

Аналогично Варианту 1, но без преимуществ обертки свойств, этот подход все равно требует запросов к базе данных для каждой темы и не обеспечивает автоматического обновления при изменении данных.


Стратегия реализации

Для оптимальной производительности с вашей связью “Тема-Вопрос”:

  1. Добавьте производный атрибут в сущность Topic:

    • Имя: correctQuestionsCount
    • Тип: Integer 16
    • Выражение: questions.@count[previousAttempt > 0]
  2. Оптимизируйте ваш основной TopicsView:

swift
struct TopicsView: View {
    @FetchRequest(
        entity: Topic.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \Topic.name, ascending: true)]
    ) private var topics: FetchedResults<Topic>
    
    var body: some View {
        List {
            ForEach(topics, id: \.objectID) { topic in
                TopicCellView(topic: topic)
            }
        }
        .onAppear {
            // Настройте предварительную выборку для лучшей производительности
            try? topics.managedObjectContext?.perform {
                let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Topic")
                fetchRequest.relationshipKeyPathsForPrefetching = ["questions"]
                // Примените к вашему существующему запросу
            }
        }
    }
}
  1. Упростите TopicCellView:
swift
struct TopicCellView: View {
    @ObservedObject private var topic: Topic
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(topic.name)
            Text("\(topic.correctQuestionsCount) правильных вопросов")
                .font(.caption)
                .foregroundColor(.secondary)
        }
    }
}

Дополнительные оптимизации производительности

Пакетная выборка

Реализуйте пакетную выборку для снижения использования памяти при загрузке больших наборов данных:

swift
@FetchRequest(
    entity: Topic.entity(),
    sortDescriptors: [NSSortDescriptor(keyPath: \Topic.name, ascending: true)],
    animation: .default
) private var topics: FetchedResults<Topic> {
    didSet {
        topics.fetchBatchSize = 20
    }
}

Предварительная выборка связей

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

swift
FetchRequest(
    entity: Question.entity(),
    sortDescriptors: [],
    predicate: NSPredicate(format: "topic == %@ AND previousAttempt > 0", argumentArray: [topic]),
    animation: .default
) private var correctQuestions: FetchedResults<Question> {
    didSet {
        correctQuestions.fetchBatchSize = 10
    }
}

Управление контекстами

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


Когда выбирать каждый подход

Подход Лучше всего подходит Особенности производительности
Производные атрибуты Часто используемые подсчеты связей ★★★★★ (Лучшая производительность)
@FetchRequest Сложная фильтрация и сортировка ★★☆☆☆ (Множественные запросы)
Подсчет в Swift Маленькие связи, простая логика ★★☆☆☆ (Памемоемкий)
Ручная выборка Одноразовые операции ★★☆☆☆ (Нет автоматического обновления)

Для вашего конкретного случая использования подсчета вопросов с previousAttempt > 0, производные атрибуты обеспечивают оптимальное решение, потому что количество:

  • Вычисляется на уровне базы данных
  • Автоматически обновляется при изменении вопросов
  • Доступно без дополнительных запросов
  • Эффективно использует память

Как последовательно показывают исследования, операции на уровне базы данных превосходят обработку в памяти для подсчета связей в приложениях Core Data.