CoreDataを使う2。

先日の続き。
iphoneアプリでデータの永続化にCoreDataの使用について、XCodeで自動生成されるコードをいじってみての整理2回目。

モデルの編集

「Use Core Data for storage」をチェックしてプロジェクトを作成していると
「xcdatamodel」という拡張子で
モデルファイル(ディレクトリ)が作られているので、これを編集します。。(XCodeでファイルを選択するとエディタが起動される。)

今回は、

Category(記事カテゴリー)とTopic(記事)という2つのEntityを定義。
CategoryとTopicは1対多となるようリレーションを設定。Category側からTopicへの関連、

TopicからCategoryの関連の両方を定義します。

それぞれのEntityは以下のように。
Category
name String
createdAt Date
updatedAt Date
Topic
name String
contents String
createdAt Date
updatedAt Date

NSFetchedResultsControllerの生成

「Use Core Data for storage」をチェックしてプロジェクトを作成していると「NSFetchedResultsController」オブジェクトにより、
TableViewデータを表示するようなコードが自動生成されます。
「NSFetchedResultsController」は、CoreDataから取得したデータの集まりを(特にTableViewで使いやすいように)管理して、データを提供する役割ももっているものです。
通常のデータベースアクセスインターフェイスではSQLの結果オブジェクトに近いものなのかな。
検索条件、並び順などを指定した「NSFetchRequest」オブジェクト(これは、SQL文と同等のものかな)、取得先の「Core Data」のデータオブジェクトの格納先である「NSManagedObjectContext」を指定することにより生成されます。
今回のプロジェクトでは、ルートのTableViewを扱う「RootTableViewController」にこの「NSFetchedResultsController」オブジェクトを生成するコードが自動生成されています。
この自動生成ソースは、EvnetというEntityを対象とする内容なので、今回作成したCategory Entityを検索対象とするように変更しました。

- (NSFetchedResultsController *)fetchedResultsController {
  
  if (fetchedResultsController != nil) {
    return fetchedResultsController;
  }
  
  /*
   Set up the fetched results controller.
   */
  // Create the fetch request for the entity.
  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
  // Edit the entity name as appropriate.
  NSEntityDescription *entity = [NSEntityDescription 
             entityForName:@"Category" 
         inManagedObjectContext:managedObjectContext];
  [fetchRequest setEntity:entity];
  
  // Set the batch size to a suitable number.
  [fetchRequest setFetchBatchSize:20];
  
  // Edit the sort key as appropriate.
  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"updatedAt" 
                                                                 ascending:NO];
  NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
  
  [fetchRequest setSortDescriptors:sortDescriptors];
  
  // Edit the section name key path and cache name if appropriate.
  // nil for section name key path means "no sections".
  NSFetchedResultsController *aFetchedResultsController 
    = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
                  managedObjectContext:managedObjectContext 
                    sectionNameKeyPath:nil 
                             cacheName:@"Root"];
  aFetchedResultsController.delegate = self;
  self.fetchedResultsController = aFetchedResultsController;
  
  [aFetchedResultsController release];
  [fetchRequest release];
  [sortDescriptor release];
  [sortDescriptors release];
  
  return fetchedResultsController;
}    

以上が変更後のコードです。NSEntityDescriptionの生成時のEntity名(entityForName)やNSFetchRequestに指定するSortDescription生成時のパラメータ(ソートキー)を変更したぐらいです。
あと、補足的なことですが。

NSEntityDescriptionオブジェクトがEntityの定義を含むオブジェクトです。これはNSManagedObjectContextから取得できます(NSManagedContext <- NSPersistentStoreCoordinator <- NSManagedObjectModel という所属関連があり、元をたどれば、NSManagedObjectModelオブジェクトから取得しているのだとおもいます。)

テーブルへの表示

TableViewへの表示は、以上のNSFetchResultsControllerから情報を取得してやればよいということです。TableViewでは、UTNSTableViewDataSourceのうち以下の3つのメソッドを定義してやればよいのですが、このコードも「RootViewController」クラス内に自動生成されていてほとんど変更不要です。

numberOfSectionsInTableView:tableView - Viewのセクション数を返す。これは、NSFetchedResultsController のセクション数を返すだけ、変更なし。
tableView:numberOfRowsInSection - セクション内のデータ数を返す。これは、NSFetchedResultsController のセクション内のオブジェクト数を返すだけ、これも変更なし。
tableView:cellForRowAtIndexPath: - データセル(UITableViewCell)を返す。これはNSFetchedResultsControllerからインデックス指定で該当行のデータオブジェクト(NSManagedObject)を取得。そのオブジェクトの適当な属性値(今回はname)をセルのテキストとして指定してやればよいということになります。

ないセルオブジェクトは、1回このメソッドで返されれば(可能な限り)キャッシュされるので、キャッシュされていない場合だけ新規作成するということになります。

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  return [[fetchedResultsController sections] count];
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  id <NSFetchedResultsSectionInfo> sectionInfo 
      = [[fetchedResultsController sections] objectAtIndex:section];
  return [sectionInfo numberOfObjects];
}


// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  
  static NSString *CellIdentifier = @"Cell";
  
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                                   reuseIdentifier:CellIdentifier] autorelease];
  }
  
  // Configure the cell.
  NSManagedObject *managedObject = [fetchedResultsController objectAtIndexPath:indexPath];
  cell.textLabel.text = [[managedObject valueForKey:@"name"] description];
  //    cell.textLabel.text = @"test";
  return cell;
}

追加

Core Dataにデータを追加する場合は、

      1. NSEntitydescriptionから新規挿入用のNSManagedObjectを生成(insertNewObjectForEntityForName:inManagedObjectContext:)して、
      2. そのNSManagedObjectに各属性値を設定(valueForKey:)
      3. NSManagedObjectを保存

という流れになります。
これも、「RootViewController」に「insertNewObject」メソッドとしてサンプルコードが自動されいますが、対象としてるEntityの定義にあわして変更が必要となります。

  NSString *name = [categoryField text];
  // Create a new instance of the entity managed by the fetched results controller.
  NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
  NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
  // 新しい永続化オブジェクトを作って
  NSManagedObject *newManagedObject 
    = [NSEntityDescription insertNewObjectForEntityForName:[entity name] 
                    inManagedObjectContext:context];
  // 値を設定(If appropriate, configure the new managed object.)
  [newManagedObject setValue:name forKey:@"name"];
  [newManagedObject setValue:[NSDate date] forKey:@"updatedAt"];
  [newManagedObject setValue:[NSDate date] forKey:@"createdAt"];
  // Save the context.
  NSError *error = nil;
  if (![context save:&error]) {
       // エラー処理 ...
  }

Todo

リレーションされている下位の情報の扱いなどについては、また今度。
セクションの扱いとかも、できえれば。。。。。