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
запроса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()
в табличном представлении.
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 только добавила возможность напрямую работать с постоянными магазинами, чтобы исправить слабые стороны фреймворка, но совет заключается в том, чтобы использовать их экономно.