Статьи

Основные данные и Swift: пакетное удаление

Core Data — это структура, с которой мне действительно нравится работать. Несмотря на то, что Core Data не идеальны, приятно видеть, что Apple продолжает инвестировать в эту инфраструктуру. В этом году, например, Apple добавила возможность пакетного удаления записей. В предыдущей статье мы обсуждали пакетные обновления. Идея, лежащая в основе удаления пакетов, очень похожа, как вы узнаете из этого урока.

Если приложению Core Data необходимо удалить большое количество записей, оно столкнется с проблемой. Хотя нет необходимости загружать запись в память, чтобы удалить ее, это просто, как работает Core Data. Как мы уже говорили в предыдущей статье , это имеет ряд недостатков. До введения пакетных обновлений не было правильного решения для обновления большого количества записей. До iOS 9 и OS X El Capitan то же самое применялось к пакетному удалению.

Хотя класс NSBatchUpdateRequest был представлен в iOS 8 и OS X Yosemite, класс NSBatchDeleteRequest был добавлен только недавно, наряду с выпуском iOS 9 и OS X El Capitan. Как и его двоюродный брат NSBatchUpdateRequest , экземпляр NSBatchDeleteRequest работает непосредственно в одном или нескольких постоянных хранилищах.

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

В предыдущем уроке мы добавили функцию, чтобы пометить каждый элемент списка дел как выполненный. Давайте вернемся к этому приложению и добавим возможность удалять каждый элемент списка дел, помеченный как выполненный.

Загрузите или клонируйте проект из GitHub и откройте его в Xcode 7. Убедитесь, что цель развертывания проекта установлена ​​на iOS 9 или выше, чтобы убедиться, что класс NSBatchDeleteRequest доступен.

Откройте ViewController.swift и объявите свойство deleteAllButton типа UIBarButtonItem . Вы можете удалить свойство checkAllButton так как оно нам не понадобится в этом руководстве.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import UIKit
import CoreData
 
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
 
    let ReuseIdentifierToDoCell = «ToDoCell»
     
    @IBOutlet weak var tableView: UITableView!
     
    var managedObjectContext: NSManagedObjectContext!
     
    var deleteAllButton: UIBarButtonItem!
 
    …
 
}

Инициализируйте элемент кнопки панели в viewDidLoad() класса ViewController и установите его в качестве элемента кнопки левой панели элемента навигации.

1
2
3
4
5
// Initialize Delete All Button
deleteAllButton = UIBarButtonItem(title: «Delete All», style: .Plain, target: self, action: «deleteAll:»)
 
// Configure Navigation Item
navigationItem.leftBarButtonItem = deleteAllButton

Использование класса NSBatchDeleteRequest не сложно, но нам нужно позаботиться о нескольких проблемах, которые свойственны прямой работе с постоянным хранилищем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func deleteAll(sender: UIBarButtonItem) {
    // Create Fetch Request
    let fetchRequest = NSFetchRequest(entityName: «Item»)
     
    // Configure Fetch Request
    fetchRequest.predicate = NSPredicate(format: «done == 1»)
     
    // Initialize Batch Delete Request
    let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
     
    // Configure Batch Update Request
    batchDeleteRequest.resultType = .ResultTypeCount
     
    do {
        // Execute Batch Request
        let batchDeleteResult = try managedObjectContext.executeRequest(batchDeleteRequest) as!
         
        print(«The batch delete request has deleted \(batchDeleteResult.result!) records.»)
         
        // Reset Managed Object Context
        managedObjectContext.reset()
         
        // Perform Fetch
        try self.fetchedResultsController.performFetch()
         
        // Reload Table View
        tableView.reloadData()
         
    } catch {
        let updateError = error as NSError
        print(«\(updateError), \(updateError.userInfo)»)
    }
}

Объект NSBatchDeleteRequest инициализируется объектом NSFetchRequest . Именно этот запрос на выборку определяет, какие записи будут удалены из постоянных хранилищ. В deleteAll(_:) мы создаем запрос на выборку для объекта Item . Мы устанавливаем свойство predicate запроса на выборку, чтобы убедиться, что мы удаляем только те записи элемента , которые помечены как выполненные.

1
2
3
4
5
// Create Fetch Request
let fetchRequest = NSFetchRequest(entityName: «Item»)
 
// Configure Fetch Request
fetchRequest.predicate = NSPredicate(format: «done == 1»)

Поскольку запрос на выборку определяет, какие записи будут удалены, у нас есть все NSFetchRequest класса NSFetchRequest , в том числе установка ограничения на количество записей, использование дескрипторов сортировки и указание смещения для запроса выборки.

Как я упоминал ранее, пакетный запрос на удаление инициализируется экземпляром NSFetchRequest . Поскольку класс NSBatchDeleteRequest является подклассом NSPersistentStoreRequest , мы можем установить свойство resultType запроса, чтобы указать, какой тип результата нас интересует.

1
2
3
4
5
// Initialize Batch Delete Request
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
 
// Configure Batch Update Request
batchDeleteRequest.resultType = .ResultTypeCount

Свойство NSBatchDeleteRequest экземпляра NSBatchDeleteRequest имеет тип NSBatchDeleteRequestResultType . NSBatchDeleteRequestResultType определяет три переменные-члены:

  • ResultTypeStatusOnly : сообщает нам, был ли запрос на пакетное удаление успешным или неудачным.
  • ResultTypeObjectIDs : это дает нам массив экземпляров NSManagedObjectID которые соответствуют записям, которые были удалены запросом пакетного удаления.
  • ResultTypeCount : установив для свойства ResultTypeCount запроса resultType ResultTypeCount , мы получаем количество записей, на которые повлиял (удален) запрос пакетного удаления.

Из предыдущего урока вы можете вспомнить, что executeRequest(_:) является методом метания. Это означает, что нам нужно заключить вызов метода в оператор do-catch . Метод executeRequest(_:) возвращает объект NSPersistentStoreResult . Поскольку мы имеем дело с запросом пакетного удаления, мы приводим результат к объекту NSBatchDeleteResult . Результат выводится на консоль.

01
02
03
04
05
06
07
08
09
10
do {
    // Execute Batch Request
    let batchDeleteResult = try managedObjectContext.executeRequest(batchDeleteRequest) as!
     
    print(«The batch delete request has deleted \(batchDeleteResult.result!) records.»)
     
} catch {
    let updateError = error as NSError
    print(«\(updateError), \(updateError.userInfo)»)
}

Если вы запустите приложение, заполните его несколькими элементами и коснитесь кнопки « Удалить все» , пользовательский интерфейс не будет обновлен. Я могу заверить вас, что запрос на пакетное удаление сработал. Помните, что контекст управляемого объекта никоим образом не уведомляется о последствиях запроса на пакетное удаление. Очевидно, это то, что нам нужно исправить.

В предыдущем уроке мы работали с классом NSBatchUpdateRequest . Мы обновили контекст управляемого объекта, обновив объекты в контексте управляемого объекта, на которые повлиял запрос на пакетное обновление.

Мы не можем использовать ту же технику для запроса пакетного удаления, потому что некоторые объекты больше не представлены записью в постоянном хранилище. Мы должны принять решительные меры, как вы можете видеть ниже. Мы вызываем reset() для контекста управляемого объекта, что означает, что контекст управляемого объекта начинается с чистого листа.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
do {
    // Execute Batch Request
    let batchDeleteResult = try managedObjectContext.executeRequest(batchDeleteRequest) as!
     
    print(«The batch delete request has deleted \(batchDeleteResult.result!) records.»)
     
    // Reset Managed Object Context
    managedObjectContext.reset()
     
    // Perform Fetch
    try self.fetchedResultsController.performFetch()
     
    // Reload Table View
    tableView.reloadData()
     
} catch {
    let updateError = error as NSError
    print(«\(updateError), \(updateError.userInfo)»)
}

Это также означает, что выбранный контроллер результатов должен выполнить выборку, чтобы обновить записи, которыми он управляет для нас. Чтобы обновить пользовательский интерфейс, мы вызываем reloadData() в табличном представлении.

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

Прежде чем продолжить, я хотел бы заполнить постоянное хранилище фиктивными данными, чтобы у нас было с чем поработать. Это облегчает визуализацию того, что я собираюсь объяснить. Добавьте следующий вспомогательный метод в ViewController.swift и вызовите его в viewDidLoad() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// MARK: —
// MARK: Helper Methods
private func seedPersistentStore() {
    // Create Entity Description
    let entityDescription = NSEntityDescription.entityForName(«Item», inManagedObjectContext: managedObjectContext)
     
    for i in 0…15 {
        // Initialize Record
        let record = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
         
        // Populate Record
        record.setValue((i % 3) == 0, forKey: «done»)
        record.setValue(NSDate(), forKey: «createdAt»)
        record.setValue(«Item \(i + 1)», forKey: «name»)
    }
     
    do {
        // Save Record
        try managedObjectContext?.save()
         
    } catch {
        let saveError = error as NSError
        print(«\(saveError), \(saveError.userInfo)»)
    }
}

В seedPersistentStore() мы создаем несколько записей и помечаем каждый третий элемент как выполненный. Обратите внимание, что мы вызываем save() в контексте управляемого объекта в конце этого метода, чтобы убедиться, что изменения передаются в постоянное хранилище. В viewDidLoad() мы viewDidLoad() постоянное хранилище.

1
2
3
4
5
6
7
8
override func viewDidLoad() {
    super.viewDidLoad()
     
    …
 
    // Seed Persistent Store
    seedPersistentStore()
}

Запустите приложение и нажмите кнопку « Удалить все» . Записи, помеченные как выполненные, должны быть удалены. Что произойдет, если вы отметите несколько оставшихся элементов как выполненные и снова нажмите кнопку « Удалить все» . Эти предметы тоже удалены? Вы можете догадаться, почему это так?

Запрос на пакетное удаление напрямую взаимодействует с постоянным хранилищем. Однако когда элемент помечается как выполненный, изменение не сразу передается в постоянное хранилище. Мы не вызываем save() в контексте управляемого объекта каждый раз, когда пользователь помечает элементы как выполненные. Мы делаем это только тогда, когда приложение перемещено в фоновый режим и завершено (см. AppDelegate.swift ).

Решение простое. Чтобы решить эту проблему, нам нужно сохранить изменения контекста управляемого объекта перед выполнением запроса пакетного удаления. Добавьте следующие строки в метод deleteAll(_:) и снова запустите приложение, чтобы протестировать решение.

01
02
03
04
05
06
07
08
09
10
11
12
13
func deleteAll(sender: UIBarButtonItem) {
    if managedObjectContext.hasChanges {
        do {
            try managedObjectContext.save()
        } catch {
            let saveError = error as NSError
            print(«\(saveError), \(saveError.userInfo)»)
        }
    }
     
    …
     
}

Подклассы NSPersistentStoreRequest являются очень полезным дополнением к платформе Core Data, но я надеюсь, что ясно, что они должны использоваться только тогда, когда это абсолютно необходимо. Apple только добавила возможность напрямую работать с постоянными магазинами, чтобы исправить слабые стороны фреймворка, но совет заключается в том, чтобы использовать их экономно.