CoreDataを使う3。リレーション

先日の続き。
iphoneアプリでデータの永続化にCoreDataの使用について、整理3回目。
今回は、リレーションをつけた場合の下位のエンティティの扱い。
今回の具体例では、Categoryに属する、Topic(記事)の表示、追加。

追加、削除のためのManagedObjectクラスを定義

リレーションの上位のEntityに対する、ManagedObjectクラス(NSManagedObjectから派生)を追加、追加と削除のメソッドを作成。上位から下位のEntityの関連を登録することができるようになります。ですが、この追加、削除のメソッドは実装を行わなくても、CoreDataGeneratedAccessorsというカテゴリーでインターフェイスだけを宣言しておければ、実装は不要です(動的に生成されます。)

カテゴリーなしのインターフェイスは、Entityの属性値にアクセスするためのPropertyだけを宣言しました。

@interface Category : NSManagedObject {
}

@property (retain) NSString *name;
@property (retain) NSDate *createdAt;
@property (retain) NSDate *updatedAt;
@property (nonatomic, retain) NSSet* topic;


@end

追加、削除のためのCoreDataGeneratedAccessorsカテゴリーは以下のとおりにしました。

@interface Category (CoreDataGeneratedAccessors)

- (void)addTopicObject:(NSManagedObject *)value;
- (void)removeTopicObject:(NSManagedObject *)value;
- (void)addTopic:(NSSet *)value;
- (void)removeTopic:(NSSet *)value;

@end

「add<下位Entity名>Object:value」,「add<下位Entitiy名>:value」、で追加、「remove<下位Entity名>Object:value」,「remove<下位Entitiy名>:value」で削除のためのメソッドを宣言します。

実装部分は、とりあえずプロパティの実装、@dynamic指定子を指定することにより、動的にメソッド定義されるようにします。
リレーション追加、削除のメソッドは、(単純な場合は)前述したように実装不要です。(モデルのエディター内のリレーション名のところで表示できるコンテキストメニューの「Objc-c 2.0 Method Implimentations をクリップボードをコピー」を選択すると、実装部分も生成してくれます。必要な場合は、これを元に追加処理を含めた実装もできるよいうことになります。)
なお、メソッドがどうやって動的につくられるかということも気になったのですが、NSObjectから定義されている「resolveInstanceMethod:(SEL)name」が、定義されたメソッドがみつからない場合に呼び出され、そこで動的にメソッド解決されているということのようです(rubyでのmetho_missingに相当?)。

追加の操作

入力されたデータをCore Dataに追加する操作は、上位のEntityリレーションに対して行った操作に加えて、上で作成したリレーション追加のメソッド呼び出しが増えることになります。

      1. NSEntitydescriptionから新規挿入用のNSManagedObjectを生成(insertNewObjectForEntityForName:inManagedObjectContext:)して、
      2. そのNSManagedObjectに各属性値を設定(valueForKey:)
      3. 上位のEntity(今回はCategory)からのリレーション追加操作を行う。
      4. NSManagedObjectを保存。

という流れになります。

- (IBAction) saveAction:(id)sender {
  NSLog(@"saveAction");
  NSString *subject = [subjectField text];
  NSString *contents = [contentsView 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:subject forKey:@"name"];
  [newManagedObject setValue:contents forKey:@"contents"];
  [newManagedObject setValue:[NSDate date] forKey:@"updatedAt"];
  [newManagedObject setValue:[NSDate date] forKey:@"createdAt"];
  // Save the context.
  NSError *error = nil;
  if ([category respondsToSelector:@selector(addTopicObject:) ] ) {
    [category addTopicObject:newManagedObject];
  }  
  // Save the context.
  NSError *error = nil;
  if (![context save:&error]) {
    // エラー処理
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
      .....
    }
}

TableViewへの表示

基本的には、リレーションなしの場合と同じように「NSFetchedResultsController」のオブジェクトを生成。
対応するインデックスの属性値をCellの値として指定してたればよいことになります。
上位のEntityのTableViewで選択されたCategoryのものだけが含まれるように条件を指定してやる必要があります。
NSPredicateオブジェクトで条件を生成、それをNSFetchRequestに渡してやることにより、その指定を行うことができます。
NSPredicateでは、"属性名 = 値"というような形で条件を指定しますが、リレーション の場合は"<関連名>.<属性名> = 値"というかたちでの指定になります。今回の場合は、Topic側からみて、categoryへの関連、その関連先のCategory Entityののname属性のマッチするものを得るということで、"category.name = <選択されたカテゴリー名>"という指定になります。

- (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:@"Topic" 
            inManagedObjectContext:self.managedObjectContext];
  [fetchRequest setEntity:entity];
  // 条件を指定
  id categoryName = [categoryObject valueForKey:@"name"];
  NSPredicate *predicate 
    = [NSPredicate predicateWithFormat:@"%K = %@", @"category.name", categoryName];
  [fetchRequest setPredicate:predicate];
  // 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:self.managedObjectContext
                    sectionNameKeyPath:nil cacheName:@"Root"];
  aFetchedResultsController.delegate = self;
  
  NSLog(@"sectios = %d",  [[aFetchedResultsController sections] count]);
  self.fetchedResultsController = aFetchedResultsController;
  
  [aFetchedResultsController release];
  [fetchRequest release];
  [sortDescriptor release];
  [sortDescriptors release];
  return  fetchedResultsController;
}    

performFetchを忘れずに

NSFetchedResultsControllerオブジェクトは初期化した段階では、データは取得されておず、
performFetchメソッドを送ることによりデータの取得がされます。今回、このperformFetchするのを忘れていて長い時間はまってしまったのでした。。。