Core Data — это структура, с которой мне действительно нравится работать. Несмотря на то, что Core Data не идеальны, приятно видеть, что Apple продолжает инвестировать в эту инфраструктуру. В этом году, например, Apple добавила возможность пакетного удаления записей. В предыдущей статье мы обсуждали пакетные обновления. Идея, лежащая в основе удаления пакетов, очень похожа, как вы узнаете из этого урока.
1. Проблема
Если приложению Core Data необходимо удалить большое количество записей, оно столкнется с проблемой. Хотя нет необходимости загружать запись в память, чтобы удалить ее, это просто, как работает Core Data. Как мы уже говорили в предыдущей статье , это имеет ряд недостатков. До введения пакетных обновлений не было правильного решения для обновления большого количества записей. До iOS 9 и OS X El Capitan то же самое применялось к пакетному удалению.
2. Решение
Хотя класс NSBatchUpdateRequest был представлен в iOS 8 и OS X Yosemite, класс NSBatchDeleteRequest был добавлен только недавно, наряду с выпуском iOS 9 и OS X El Capitan. Как и его двоюродный брат NSBatchUpdateRequest , экземпляр NSBatchDeleteRequest работает непосредственно в одном или нескольких постоянных хранилищах.
К сожалению, это означает, что пакетные удаления страдают от тех же ограничений, что и пакетные обновления. Поскольку запрос пакетного удаления напрямую влияет на постоянное хранилище, контекст управляемого объекта не знает о последствиях запроса пакетного удаления. Это также означает, что проверки не выполняются и уведомления не публикуются, когда базовые данные управляемого объекта изменяются в результате запроса на пакетное удаление. Несмотря на эти ограничения, правила удаления для отношений применяются Базовыми данными.
3. Как это работает?
В предыдущем уроке мы добавили функцию, чтобы пометить каждый элемент списка дел как выполненный. Давайте вернемся к этому приложению и добавим возможность удалять каждый элемент списка дел, помеченный как выполненный.
Шаг 1: Настройка проекта
Загрузите или клонируйте проект из GitHub и откройте его в Xcode 7. Убедитесь, что цель развертывания проекта установлена на iOS 9 или выше, чтобы убедиться, что класс NSBatchDeleteRequest доступен.
Шаг 2. Создание элемента кнопки панели
Откройте 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
|
Шаг 3. Реализация deleteAll(_:)
Использование класса 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запросаresultTypeResultTypeCount, мы получаем количество записей, на которые повлиял (удален) запрос пакетного удаления.
Выполнить запрос на пакетное обновление
Из предыдущего урока вы можете вспомнить, что 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() в табличном представлении.
4. Сохранение состояния перед удалением
Важно быть осторожным, когда вы напрямую взаимодействуете с постоянными магазинами. Ранее в этой серии я писал, что нет необходимости сохранять изменения контекста управляемого объекта при каждом добавлении, обновлении или удалении записи. Это утверждение остается верным, но оно также имеет последствия при работе с подклассами 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 только добавила возможность напрямую работать с постоянными магазинами, чтобы исправить слабые стороны фреймворка, но совет заключается в том, чтобы использовать их экономно.