본문 바로가기

학습 노트/iOS (2021)

187 ~ 189. Fetch Request

Fetch : CoreData에서 데이터를 읽어 오는 것

Fetch Request : 읽어올 데이터의 종류, Filtering 조건, 정렬 방식 등을 포함하는 객체
Request로 불리는 이유는 이를 직접 처리하지 않고 Context에 요청하고, Context가 대신해서 이를 처리하기 때문

Fetch Request 생성하기

FetchAllViewController.swift

class FetchAllViewController: UITableViewController {
   
   var list = [NSManagedObject]()
   
   @IBAction func fetch(_ sender: Any?) {
      let context = DataManager.shared.mainContext
      
      
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      fetch(nil)
   }
}

fetchRequest 생성하기

fetchRequest를 생성하는 방식은 총 4가지다.

NSFetchRequest 인스턴스 생성후, Entity를 속성으로 지정하기.

   @IBAction func fetch(_ sender: Any?) {
	let context = DataManager.shared.mainContext
	
	let request = NSFetchRequest<NSManagedObject>()
	let entity = NSEntityDescription.entity(forEntityName: "Employee", in: context)

   }

해당 방식은 NSFetchRequest 인스턴스를 생성할 때
Generic Class를 사용하기 때문에 가져올 형식을 형식 파라미터로 지정해야한다.

let request = NSFetchRequest()

따라서 위의 형태가 아닌

let request = NSFetchRequest<NSManagedObject>()

의 방식으로 생성하게 된다.

Entity 생성에는 NSEntityDescription의 entity(forEntityName:in:) 메서드를 사용한다.
첫번째 파라미터로 Entity의 이름을 전달하고,
두번째 파라미터로 대상 context를 전달한다.

해당 방식은 FetchRequest와 Entity를 별도로 생성해야 하기에 상대적으로 번거롭고,
문자열로 Entity의 이름을 전달해야 하기 때문에 오탈자에 의한 오류 발생의 위험이 높다.

NSFetchRequest

NSEntityDescription

 

Apple Developer Documentation

 

developer.apple.com

entity(forEntityName:in:)

 

Apple Developer Documentation

 

developer.apple.com

 

Entity 인스턴스를 만들지 않고 생성자로 전달하기.

   @IBAction func fetch(_ sender: Any?) {
	let context = DataManager.shared.mainContext
	
	let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")
   }

첫번째 방법보다 간결해 졌지만
여전히 entity 이름을 문자열로 전달하기 때문에 오탈자로 인한 요류 발생의 위험이 높다.

Type 메서드 사용하기

   @IBAction func fetch(_ sender: Any?) {
	let context = DataManager.shared.mainContext
	
	let request: NSFetchRequest<EmployeeEntity> = EmployeeEntity.fetchRequest()
   }

해당 방식은 두번째 보다도 간결하고,
Entity의 이름을 문자열로 전달하지 않기 때문에 오탈자로 인한 오류 발생의 위험도 없다.
단 이 경우 형식추론을 사용할 수 없어 fetchRequest 메서드에서 반환하는 값의 형식을 알 수 없기 때문에,
위와 같이 request 인스턴스 자체의 형식을 지정해야 함을 기억한다.

StoredFetch Request

해당 방식은 추후에 다룬다.

4가지 방식 중 어느 것이든 문제 없지만 두번째와 세번째 방식이 사용 빈도가 높은 편이다.

이렇게 생성한 fetchRequest들은 다음과 같이 excute 메서드를 사용해 직접 실행하는 것이 가능하다.

   @IBAction func fetch(_ sender: Any?) {
	let context = DataManager.shared.mainContext
	
	let request: NSFetchRequest<EmployeeEntity> = EmployeeEntity.fetchRequest()
	request.execute()
   }

하지만 이전에 언급했던 것과 같이 보통은 context에 요청해 처리하는 것이 일반적이다.

   @IBAction func fetch(_ sender: Any?) {
	let context = DataManager.shared.mainContext
	
	let request: NSFetchRequest<EmployeeEntity> = EmployeeEntity.fetchRequest()
	
	do {
	   try list = context.fetch(request)
	   tableView.reloadData()
	} catch {
	   print(error.localizedDescription)
	}
   }

위와 같이 context에서 제공하는 fetch 메서드에 request를 전달하는 것으로 완료된다.
해당 메서드는 request에서 요정한 데이터를 배열로 반환하게 된다.
fetch를 사용할 때는 반드시 do-catch 문을 사용해야한다.

 

SortTableViewController.swift

import UIKit
import CoreData

class SortTableViewController: UITableViewController {
   
   var list = [NSManagedObject]()
   
   @IBAction func showMenu(_ sender: Any) {
      showSortMenu()
   }
   
   func sortByNameASC() {
      
   }
   
   func sortByNameDESC() {
      
   }
   
   func sortByAgeThenBySalary() {
      
   }
   
   func fetch(sortDescriptors: [NSSortDescriptor]? = nil) {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")

      request.sortDescriptors = sortDescriptors

      do {
         list = try DataManager.shared.mainContext.fetch(request)
         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      fetch()
   }
}


extension SortTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      let target = list[indexPath.row]
      if let name = target.value(forKey: "name") as? String, let salary = target.value(forKey: "salary") as? Int {
         cell.textLabel?.text = "[$\(salary)] \(name)"
      }
      
      if let age = target.value(forKey: "age") as? Int {
         cell.detailTextLabel?.text = "\(age)"
      }
      
      return cell
   }
}


extension SortTableViewController {
   func showSortMenu() {
      let alert = UIAlertController(title: "Sort", message: "Select sort type", preferredStyle: .alert)
      
      let nameASC = UIAlertAction(title: "name ASC", style: .default) { (action) in
         self.sortByNameASC()
      }
      alert.addAction(nameASC)
      
      let nameDESC = UIAlertAction(title: "name DESC", style: .default) { (action) in
         self.sortByNameDESC()
      }
      alert.addAction(nameDESC)
      
      let ageAndSalary = UIAlertAction(title: "age ASC salary DESC", style: .default) { (action) in
         self.sortByAgeThenBySalary()
      }
      alert.addAction(ageAndSalary)
      
      let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
      alert.addAction(cancel)
      
      present(alert, animated: true, completion: nil)
   }
}

해당 파일에는 이전에 구현한 fetch 메서드가 그대로 구현돼있다.

정렬 방식은 NSSortDescriptor를 사용한다.

오름차

  func sortByNameASC() {
	   let sortByNameASC = NSSortDescriptor(key: "name", ascending: true)
	   fetch(sortDescriptors: [sortByNameASC])
   }

NSSortDescriptor를 사용해 정렬 방식을 정의한다.
첫번째 파라미터로 정렬의 기준이 될 Attr의 이름을 전달한다.
두번째 파라미터로 오름차순이나 내림차순의 정렬 방식을 전달한다.

정의된 정렬 방식을 fetch 메서드에 전달해
데이터를 가져 올 때 정의한 정렬 규칙을 적용하도록 한다.
해당 fetch 메서드는 정렬이 필요한 경우 SortDescriptor를 배열로 전달 받을 수 있도록 정의돼있다.

NSSortDescriptor(key:ascending:)

 

Apple Developer Documentation

 

developer.apple.com

내림차

   func sortByNameDESC() {
	let sortByNameDESC = NSSortDescriptor(key: "name", ascending: false)
	fetch(sortDescriptors: [sortByNameDESC])
   }

내림차순 정렬도 같은 방식으로 정의하고 사용한다.
NSSortDescriptor의 두번째 파라미터가 true이면 오름차로, false이면 내림차로 순서가 적용된다.

정렬 방식을 정의할 때 지금과 같이 정렬 기준을 문자열로 전달하는 것이 일반적이지만,
항상 그렇듯 문자열 사용에는 오탈자로 인한 오류 발생의 위험이 존재한다.

   func sortByNameDESC() {
	   let sortByNameDESC = NSSortDescriptor(key: #keyPath(EmployeeEntity.name), ascending: false)
	   fetch(sortDescriptors: [sortByNameDESC])
   }

따라서 이렇게 keyPath를 사용해 문자열로 인한 단점을 개선하는 방식도 존재한다.

복수의 정렬 방식 적용하기

정렬 방식은 fetch 메서드에 배열로 전달되는 만큼 복수개의 방식을 적용하는 것도 가능하다.

   func sortByAgeThenBySalary() {
	   let sortByAgeASC = NSSortDescriptor(key: #keyPath(EmployeeEntity.age), ascending: true)
	   let sortBySalaryDESC = NSSortDescriptor(key: #keyPath(EmployeeEntity.salary), ascending: false)
	   fetch(sortDescriptors: [sortByAgeASC, sortBySalaryDESC])
   }

위와 같이 두 개의 규칙을 만들어 적용하게 되면
EmployeeEntity의 데이터를 나이 순으로 오름차 정렬한 뒤,
같은 나이라면 연봉 순으로 내림차 정렬하게 된다.
이 때 배열에 전달되는 순서가 정렬이 적용 되는 순서가 되게 된다.

 

ResultTypesViewController.swift

import UIKit
import CoreData

class ResultTypesViewController: UIViewController {
   
   let context = DataManager.shared.mainContext
   
   @IBAction func fetchManagedObject(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)                        
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   @IBAction func fetchCount(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   @IBAction func fetchDictionary(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   @IBAction func fetchManagedObjectID(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }
}

fetch를 통해 반환되는 결과의 형식이 항상 같은 건 아니다.
ManagedObject, Count, Dictionary, ObjectID를 반환 받을 수 있는
각각의 메서드를 제공한다.

fetchRequest의 resultType 속성을 변경해서 지정하며,
기본 값은 ManagedObject 이다.

.managedObjectResultType

   @IBAction func fetchManagedObject(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
	   request.resultType = .managedObjectResultType
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)                        
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }

resultType 속성을 managedObjectResultType으로 지정하면 기본상태와 같은
ManagedObject 배열을 반환한다.

.countResultType

   @IBAction func fetchCount(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
	   request.resultType = .countResultType
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }

resultType 속성을 countResultType으로 지정하면 Entity에 존재하는 데이터의 수를 반환한다.

   @IBAction func fetchCount(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
	   request.resultType = .countResultType
	   let count = try context.count(for: request)
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }

하지만 반환하는 값인 '데이터의 수' 자체가 목적이라면
context가 제공하는 count(for:)메서드가 더 유리할 수 있다.

.dictionaryResultType

   @IBAction func fetchDictionary(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
	   request.resultType = .dictionaryResultType
	   request.propertiesToFetch = ["name", "address"]
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }

일반적인 fetch가 모든 데이터를 읽어 오는 반면,
ManagedObject의 기능을 상실하는 대신 특정 데이터만 읽어와 자원을 절약할 수 있다.
읽어 올 데이터의 지정은 propertiesToFetch 속성에 해당하는 Attribute를 배열로 전달해 지정한다.

.managedObjectIDResultType

   @IBAction func fetchManagedObjectID(_ sender: Any) {
      let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Employee")
	   request.resultType = .managedObjectIDResultType
      
      do {
         let list = try context.fetch(request)
         if let first = list.first {
            print(type(of: first))
            print(first)
         }
      } catch {
         fatalError(error.localizedDescription)
      }
   }

모든 ManagedObject는 고유한 ID를 가진다.
해당 ID는 일반적으로 사용하는 경우가 드믈지만 데이터를 주고 받을 때 제한적으로 사용한다.

 

Fetch Request with Paging Table View

Table View와 Fetch를 사용해 paging을 구현한다.

PagingViewController.swift

import UIKit
import CoreData

class PagingViewController: UIViewController {
   
   let context = DataManager.shared.mainContext
   var offset = 0
   
   var list = [NSManagedObject]()
   
   @IBOutlet weak var listTableView: UITableView!
   
   @IBOutlet weak var pageLabel: UILabel!
   
   @IBAction func prev(_ sender: Any) {
      offset = max(offset - 1, 0)
      fetch()
   }
   
   @IBAction func next(_ sender: Any) {
      offset = offset + 1
      fetch()
   }
   
   func fetch() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")

      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
      do {
         list = try context.fetch(request)
         listTableView.reloadData()
         pageLabel.text = "\(offset + 1)"
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      fetch()
   }
}

extension PagingViewController: UITableViewDataSource {
   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      cell.textLabel?.text = list[indexPath.row].value(forKey: "name") as? String
      
      return cell
   }
}

화면 하단의 버튼은 동일한 이름의 Action 메서드와 연결돼있다.
fetch 메서드는 employee의 모든 데이터를 읽어 와 이름 순서대로 정렬해 표시하고있다.

paging의 기능 구현에는 fetchLimit과 fetchOffset 속성을 활용한다.

   func fetch() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")
	   
	   request.fetchLimit = 10

      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
      do {
         list = try context.fetch(request)
         listTableView.reloadData()
         pageLabel.text = "\(offset + 1)"
      } catch {
         fatalError(error.localizedDescription)
      }
   }

fetchLimit은 fetch 한 번에 읽어 올 데이터의 양을 결정한다.
위와 같이 10으로 지정하면, 한 번에 10개의 데이터만 읽어오게 된다.

   func fetch() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")
	   
	   request.fetchLimit = 10
	   request.fetchOffset = offset

      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
      do {
         list = try context.fetch(request)
         listTableView.reloadData()
         pageLabel.text = "\(offset + 1)"
      } catch {
         fatalError(error.localizedDescription)
      }
   }

fetchOffset은 반환되는 첫번째 데이터의 index를 결정한다.
기본값은 0으로 첫번째 데이터 부터 반환한다.
만약 1로 지정한다면 첫번째 데이터를 건너뛰고 두번째 데이터부터 반환한다.
현재 fetchOffset으로 지정한 offset 변수는 화면 하단의 버튼을 누를 때 마다 증가하거나 감소한다.

해당 방식은 fetch 자체에 제한을 걸게 되기 때문에 자원을 효율적으로 사용하고, 속도도 향상된다.

하지만 지금과 같이 paging 자체로 구현하기 보다는 정렬 후 필요한 상위 데이터를 취하는데 사용되는 경우가 더 잦다.

paging 방식 자체는 모바일 환경에 치화적이지 않다.
그렇다고 지금까지와 같이 fetchLimit을 지정하지 않으면 항상 모든 데이터를 읽어 오기 때문에
이 또한 적절하지 않다.

TableView로 데이터를 부분 로드하는 일반적인 메커니즘은 다음과 같다.

  • 화면의 마지막 Cellㅇ 표시되면 offset을 증가시킨다.
  • 새로 fetch를 실행한다.
  • fetch가 완료되면 tableView에 반영한다.

해당 메커니즘을 매번 구현하는 것은 상당히 복잡한 일이기 때문에 Swift는 이를 fetchBatchSize라는 이름으로 제공한다.
fetchBatchSize는 한 번에 읽어 올 양을 저장해 두면 필요할 때 마다 해당 양을 자동으로 불러 오도록 구현돼있다.

BatchSizeTableViewController.swift

import UIKit
import CoreData

class BatchSizeTableViewController: UITableViewController {
   
   let context = DataManager.shared.mainContext
   
   var list = [NSManagedObject]()
   
   @IBAction func showType(_ sender: Any) {
      showTypeMenu()
   }
   
   func fetchWithBatchSize() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")
      
      request.fetchBatchSize = 30
      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
      do {
         list = try context.fetch(request)
         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   func fetchWithoutBatchSize() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")
      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
      do {
         list = try context.fetch(request)
         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }
}


extension BatchSizeTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      cell.textLabel?.text = list[indexPath.row].value(forKey: "name") as? String
      
      return cell
   }
}

extension BatchSizeTableViewController {
   func showTypeMenu() {
      let alert = UIAlertController(title: "Batch Size", message: "Select request type", preferredStyle: .alert)
      
      let menu1 = UIAlertAction(title: "Without Batch Size", style: .default) { (action) in
         self.fetchWithoutBatchSize()
      }
      alert.addAction(menu1)
      
      let menu2 = UIAlertAction(title: "With Batch Size", style: .default) { (action) in
         self.fetchWithBatchSize()
      }
      alert.addAction(menu2)
      
      let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
      alert.addAction(cancel)
      
      present(alert, animated: true, completion: nil)
   }
}

구현돼있는 메서드 fetchWithBatchSize는 BatchSize를 사용하고,
fetchWithoutBatchSize는 BatchSize를 사용하지 않고 fetch를 구현한다.

fetchBatchSize를 지정하면 한 번에 지정된 양 만큼만 데이터를 읽어 오고,
필요해지면 다시 같은 양을 읽어오도록 구현돼있다.
이는 육안 상으로 차이가 나지 않지만 내부적인 변화에 해당한다.

Xcode 메뉴 > Product > Profile > CoreData
에서 시뮬레이션 하며 변화르 확인 할 수 있다.

  fetch Count fetch Duration 비고
fetchBatchSize X 5000 15.65ms  
fetchBatchSize O 5000 5.98ms 총 데이터 수 확인
30 1.62ms 실제 fetch
30 416.12µs  

 

AsyncFetchingTableViewController

CoreData의 성능이 높기 때문에 mainContext에서 실행해도 ThreadBlocking의 가능성은 적음
fetch 메서드는 '동기' 메서드로 완료가 될 때 까지 Thread 점유를 해제하지 않는다.
따라서 Data의 양이 늘면 언젠가는 사용자가 인지하는 딜레이가 생기게 된다.

해당 문제는 BackgroundContext의 사용으로 해결할 수 있다.
단, 이 경우 코드의 복잡도가 증가하는 것은 피할 수 없다.

대신 fetch를 '비동기'방식으로 전환하면 BackgroundContext를 사용하지 않아도 문제를 일부 해결할 수 있다.

import UIKit
import CoreData

class AsyncFetchingTableViewController: UITableViewController {
   
   var list = [NSManagedObject]()
   
   @IBAction func fetch(_ sender: Any?) {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")

      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]

      do {
         list = try DataManager.shared.mainContext.fetch(request)
         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      fetch(nil)
      tableView.reloadData()
   }
}


extension AsyncFetchingTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      cell.textLabel?.text = list[indexPath.row].value(forKey: "name") as? String
      
      return cell
   }
}
 @IBAction func fetch(_ sender: Any?) {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Employee")

      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
	   
	   let asyncRequest = NSAsynchronousFetchRequest<NSManagedObject>(fetchRequest: request) { (result) in
		   guard let list = result.finalResult else {
			   return
		   }
		   self.list = list
		   self.tableView.reloadData()
	   }

      do {
         list = try DataManager.shared.mainContext.fetch(request)
         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }

fetch 메서드 내에서 Asynchronous Request를 생성한다.
비동기방식 Request를 생성할 때는 NSAsynchrounousFetchRequest를 사용한다.
해당 클래스는 generic 클래스로 타입을 지정해 사용해야한다.

NSAsynchrounousFetchRequest 클래스의 NSAsynchrounousFetchRequest(fetchRequest:completionBlock:)
메서드를 사용해 생성하게 된다.
첫번째 파라미터는 비동기 방식 fetchRequet로 변경할 일반 fetchRequest를 전달한다.
두번째 파라미터는 완료후 동작을 블록으로 전달한다.

해당 블록에는 NSAsynchronousFetchRequest의 결과를 전달하게 된다.

NSAsynchrounousFetchRequest(fetchRequest:completionBlock:)

 

Apple Developer Documentation

 

developer.apple.com

완료된 결과에 접근하기 위해서 finalResult 속성을 사용하는 것에 주의한다.

해당 메서드는 비동기 방식이기 때문에 stack에 전달 후 즉시 Thread를 반환하게 된다.
따라서 fetch가 아닌 execute 메서드를 사용한다.

비동기 방식으로 fetch를 실행하게 되면

  • 진행 상황을 실시간으로 확인 할 수 있다.
  • 진행중인 fetch를 취소할 수 있다.

위의 장점을 기대해 볼 수 있지만,
Core Data의 성능이 워낙 좋은 편이기 때문에 사용 빈도는 낮다.

 

Stored Fetch Request, Fetched Property

코드가 아닌 DataModel에서 Fetch Request를 생성하고 사용하는 fetchRequest다.

Sample.xcdatamodeld

Add Entity 버튼을 길게 클릭해 Add Fetch Request를 선택한다.

fetch Request를 설정한다.
이름을 변경하고,
Entity는 fetch 대상으로 설정한다.
Fetch limit은 한 번에 읽어올 데이터의 수를 지정한다. 100으로 설정했다.

fetchRequet 오른쪽의 '+' 버튼을 눌러 조건을 추가한다.
salary를 기준값으로 80000 이상의 값을 읽어오게 된다.

StoredFetchRequestTableViewController

import UIKit
import CoreData

class StoredFetchRequestTableViewController: UITableViewController {
   
   var list = [NSManagedObject]()
   
   func fetch() {
      
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      fetch()
   }
}


extension StoredFetchRequestTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      let target = list[indexPath.row]
      if let name = target.value(forKey: "name") as? String, let deptName = target.value(forKeyPath: "department.name") as? String {
         cell.textLabel?.text = "\(name)\n\(deptName)"
      }
      
      cell.detailTextLabel?.text = "$ \((target.value(forKey: "salary") as? Int) ?? 0)"
      
      return cell
   }
}
   func fetch() {
	   guard let model = DataManager.shared.container?.managedObjectModel else {
		   fatalError()
	   }
   }

storedFetchRequest를 사용하기 때문에 따로 request를 생성하지 않고
CoreData 내의 request를 그대로 사용한다.
따라서 Container에서 managedObjectModel을 불러온다.

   func fetch() {
	   guard let model = DataManager.shared.container?.managedObjectModel else {
		   fatalError()
	   }
	   guard let request = model.fetchRequestTemplate(forName: "highSalary") as? NSFetchRequest<NSManagedObject> else {
		   fatalError("Not Found")
	   }
   }

StroedFetchRequest를 불러 오는 방법은 총 3개다.

fetchRequestTemplatesByName
생성된 Fetch Request의 이름을 반환한다.

fetchRequestTemplate(withName:)
이름으로 Fetch Request에 접근한다.

fetchRequestFromTemplate(withName:SubstitutionVariables:)
대체 가능한 변수가 있을 경우 파라미터로 함께 전달한다.

실습에선 두번째 방법을 사용한다.

   func fetch() {
	   guard let model = DataManager.shared.container?.managedObjectModel else {
		   fatalError()
	   }
	   guard let request = model.fetchRequestTemplate(forName: "highSalary") as? NSFetchRequest<NSManagedObject> else {
		   fatalError("Not Found")
	   }
	   
	   do {
		   try list = DataManager.shared.mainContext.fetch(request)
		   tableView.reloadData()
	   } catch {
		   print(error)
	   }
   }

생성된 request를 이전과 같은 방법으로 fetch에 전달하고 TableView를 새로고침한다.

결과는 80000 달러 이상의 연봉을 가진 직원이 제대로 출력되지만,
정렬이 되진 않는다.
StoredFetchRequest는 정렬을 별도로 설정할 수 없기 때문에 코드로 적용해 줘야한다.

   func fetch() {
	   guard let model = DataManager.shared.container?.managedObjectModel else {
		   fatalError()
	   }
	   guard let request = model.fetchRequestTemplate(forName: "highSalary") as? NSFetchRequest<NSManagedObject> else {
		   fatalError("Not Found")
	   }
	   
	   let sortBySalary = NSSortDescriptor(key: #keyPath(EmployeeEntity.salary), ascending: true)
	   request.sortDescriptors = [sortBySalary]
	   
	   do {
		   try list = DataManager.shared.mainContext.fetch(request)
		   tableView.reloadData()
	   } catch {
		   print(error)
	   }
   }

NSSortDescriptor를 사용해 정렬 규칙을 생성하고,
sortDescriptors 속성에 배열로 전달해 적용될 수 있도록 한다.

하지만 지금 상태로는 다시 충돌이 발생한다.

이는 dataModel 자체가 불변객체이기 때문에 dataModel에 속한 FetchRequest도 불변객체가 되기 때문이다.

  func fetch() {
	   guard let model = DataManager.shared.container?.managedObjectModel else {
		   fatalError()
	   }
//	   guard let request = model.fetchRequestTemplate(forName: "highSalary") as? NSFetchRequest<NSManagedObject> else {
//		   fatalError("Not Found")
//	   }
	   
	   guard let request = model.fetchRequestFromTemplate(withName: "highSalary", substitutionVariables: ["deptName": "Dev"]) as? NSFetchRequest<NSManagedObject> else {
		   fatalError("Invalid Model")
	   }
	   
	   let sortBySalary = NSSortDescriptor(key: #keyPath(EmployeeEntity.salary), ascending: true)
	   request.sortDescriptors = [sortBySalary]
	   
	   do {
		   try list = DataManager.shared.mainContext.fetch(request)
		   tableView.reloadData()
	   } catch {
		   print(error)
	   }
   }

따라서 Request 자체를 전달하는 것이 아니라 사본을 전달해 수정하고 대체하는 방식으로 구현해야한다.
fetchRequestFromTemplate(withName:substitutionVariables:) 메서드를 사용해 사본을 복사해 가져와 사용하면 문제는 해결된다.

Filtering 조건 추가

Sample.xcmodeld

다시한 번 StoredFetchRequest를 선택하고 오른쪽의 '+를 선택해 Predicate를 추가한다.
Predicate는 Filtering의 조건으로 문법에 맞게 정확히 적용해야 한다.
실습에서는 "department.name BEGINSWITH $deptName"라고 적용했다.

이 중 '$'기 붙은 부분은 변수 취급을 의미한다.

StoredFetchRequest의 상단에 존재하는 박스에는 세개의 옵션이 존재한다.

None
조건을 반전한다.

All
And와 같다.

Any
Or과 같다.

StoredFetchRequesTableViewController.swift

이제 이해 할 수 없었던 fetchRequestFromTemplate(withName:substitutionVariables:) 메서드의
두번째 파라미터로 전달됐던 Dictionary 변수에 대한 의문이 해결 됐다.

변수 취급된 deptName을 대신하는 데이터가 바로 두번째 파라미터이다.

Sample.xcmodeld 파일의 Department Entity에서
연봉이 30000 이하인 직원을 반환하는 Request를 추가한다.
이름을 변경하고,

Destination을 대상 Entity로 변경한다.
이후 predicate를 조건에 맞게 수정한다.
이 때 predicate에서 가리키는 Attribute는 반드시 Destination에 존재하는 Attribute여야 한다.

이렇게 되면 현재 Entity와 Fetch Destination으로 지정한 Entity와의 가상의 Relation이 생성된다.

Department의 Codegen이 자동이지만,
이번에 추가한 Fetched Property는 자동으로 생성되지 않기 때문에 작업이 필요하다.

FetchedPropertyTableViewController.swift

import UIKit
import CoreData

class FetchedPropertyTableViewController: UITableViewController {
   var list = [NSManagedObject]()
   
   func fetch() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Department")
      request.fetchLimit = 1
      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
      do {
//         if let first = try DataManager.shared.mainContext.fetch(request).first as? DepartmentEntity {
//            navigationItem.title = first.name
//            
//
//         }
//         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }
   
   override func viewDidLoad() {
      super.viewDidLoad()
      
      fetch()
   }
}


extension FetchedPropertyTableViewController {
   override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return list.count
   }
   
   override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
      
      let target = list[indexPath.row]
      if let name = target.value(forKey: "name") as? String, let deptName = target.value(forKeyPath: "department.name") as? String {
         cell.textLabel?.text = "\(name)\n\(deptName)"
      }
      
      cell.detailTextLabel?.text = "$ \((target.value(forKey: "salary") as? Int) ?? 0)"
      
      return cell
   }
}

 

우선은 Department의 lowSalary에 접근해야 한다.

   func fetch() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Department")
      request.fetchLimit = 1
      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
	   if let model = DataManager.shared.container?.managedObjectModel, let entity = model.entitiesByName["Department"], let property = entity.propertiesByName["lowSalary"] as? NSFetchedPropertyDescription
      do {
//         if let first = try DataManager.shared.mainContext.fetch(request).first as? DepartmentEntity {
//            navigationItem.title = first.name
//            
//
//         }
//         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }

이후 fetch를 실행하고 결과를 NSManagedObject 배열로 저장한다.

   func fetch() {
      let request = NSFetchRequest<NSManagedObject>(entityName: "Department")
      request.fetchLimit = 1
      
      let sortByName = NSSortDescriptor(key: "name", ascending: true)
      request.sortDescriptors = [sortByName]
      
	   if let model = DataManager.shared.container?.managedObjectModel, let entity = model.entitiesByName["Department"], let property = entity.propertiesByName["lowSalary"] as? NSFetchedPropertyDescription, let fetchedRequest = property.fetchRequest {
		   let sortBySalary = NSSortDescriptor(key: #keyPath(EmployeeEntity.salary), ascending: true)
		   fetchedRequest.sortDescriptors = [sortBySalary]
	   }
      do {
//         if let first = try DataManager.shared.mainContext.fetch(request).first as? DepartmentEntity {
//            navigationItem.title = first.name
//            
//
//         }
//         tableView.reloadData()
      } catch {
         fatalError(error.localizedDescription)
      }
   }

지금 상태로는 정상적으로 동작하지만 정렬은 적용되지 않은 상태이다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

191. Predicate Syntax  (0) 2022.03.24
190. Predicate  (0) 2022.03.24
187. Entity Hierarchy, Relationships  (0) 2022.02.18
186. Managed Object and Managed Object Context  (0) 2022.02.12
185. CoreData  (0) 2022.02.05