Study CoreData 19 ~最悪のシナリオ~

いきなり重いタイトルですみません<(_ _;)>

こんばんわんば!Jacminikです。
今回はいつもとはちょっと違った内容になりそうです。
 
 

今日のエントリーは前回の最後に触れた通り、僕自身がdotodoを作るにあたって
最もハマってしまったポイント
を解説していく事になります。

 
実はたったひとつの解決法が見つけられないせいで、恐らく2〜3週間くらいずっとおんなじ場所を直してはビルド、直してはビルド…を何度も何度も繰り返していました。
 
 
今後、みなさんが同じポイントでハマらずに済む事を願いつつ、
『Study CoreData 19 ~最悪のシナリオ~』解禁です!

注意:投稿者自身もCoraDataについて勉強中のため、このシリーズには誤りが含まれている可能性があります。もし、間違いに気付かれた方はコメント欄もしくはtwitterなどでご指摘いただけると幸いです<(_ _)>
また、開発環境はXcode3.2.3 iPhone SDK 4です。実機でのテストなどは自己責任でお願いいたします。

 
 

1. 元凶

前回は、起動時にちゃんと意図したソートが出来るようにしましたね。

で、そのあと色々とテストしてみた方は分かったと思うのですが
今のままの状態で色々とデータを編集していくと、
突然テーブルビューが真っ白になって何も表示されない状態になる事があります。

そしてこんなエラーが吐かれます。

*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1261.5/UITableView.m:920

Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (3 inserted, 0 deleted). with userInfo (null)2

 
 
Serious application errorって…
 
つまり…重大な欠陥
 
大体こんな内容のようです。

セクション0の行数が無効です。
そのセクションから削除/挿入された行数(挿入3, 削除0)をプラスマイナスした
アップデート(3)実行後の既存セクションの行数は、アップデート(2)実行前の同セクションの行数と等しくなければなりません。

 
どういう事かと言うと、
例えばindexPath(0, 0)のセルがindexPath(1, 2)に移動する場合、実際は単に移動するのではなく
indexPath(0, 0)のセルを一旦削除してからindexPath(1, 2)にインサートすることで移動したように見せているのですが、
データの編集によってセルが移動した際に削除されたセルの数とインサートされたセルの数があっていないことでテーブルビューがうまく更新できずに真っ白になってしまう。
 
 
でも、コレってAppleのサンプルコードとかでは当然ながら起きない事なんです。
そのコードの中で特別対策をしている箇所もありませんし。
 
 
先に白状しておきます^^;
このエラーが出る大もとの元凶は、実は僕の計画にありました。

なんとなく決めた『Categoryでのセクション分け』『Tasksの多い順にソート』という点に大きな問題が潜んでいたのです。
 
 

なので、みなさんが実際にCoreDataを使ったアプリを作る際には
問題なくソート出来るかどうか?
セクションとソートの関係にムリはないか?
などを充分に考慮に入れた上でデータの構造を考えた方が今回のような問題にハマりにくくなると思います。

(※今回のように、セクションも含めたソート順が激しく動的に変動するような仕様は出来るだけ避けた方が無難かと。)
 
 
ですが、dotodoではその問題が分かった上で構造を変える事はしませんでした。
理由は『CoreData習得のためのテストプロジェクト』だからです。

今回はとことんハマって、エラーの解決策を探っていくことにしましたとさ。
 
 

2. Google先生もお手上げ?

まず、どこを修正すれば直せるのか?
これはなんとなく見当がつきました。

エラーの中に
An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:
とあるように[controllerDidChangeContent:]に行くまでの間。

つまり、
[controllerWillChangeContent:]
[controller: didChangeSection…]
[controller: didChangeObject…]
[controllerDidChangeContent:]
この四つのメソッドでの処理を修正すれば直せそうです。

ですが、持っている本を読み返してもググってみても、惜しい情報は見つかるんですがどうも解決策には至らないものばかり…
 
 
…で、これらのメソッドで行われているパターンをひたすらログを取りつつメモを取り調べていきました。
ここで、特に重要だと分かったのは[controller: didChangeObject…]でのオブジェクトの移動時の処理です。
 
まず、以下の画像を見てみてください。

画像上の状態が今のdotodoの状態です。
条件として、categoryが3つでそれぞれ2つのtaskとの関連を持っています。
で、この中の[B-0]というtaskを”Category C”に変更したらどうなるか?を示している図です。

赤の矢印は[controller: didChangeObject…]内で送られてきた“indexPath”(移動元)“newIndexPath”(移動先)で、番号は呼ばれた順番になります。
(※現状での処理は、indexPathを削除してnewIndexPathを挿入。)
 
 
つまりこの場合、
2と3の移動でsection:1は空になりますが、section自体は残ったままになり
元々あるsection:0(Category A)は移動されないのでsection:0に残ります。

…で、結果的にどうなるかと言うと

section:0は、元々あった”Category A”のtaskと移動してきた3つのtask(CategoryはCになる)が混在している状態。
さらにsection:1はtaskを持たないので“カテゴリーがない”状態。
section:2だけは正常に移動している。

なので、tableViewはどう表示していいのかが分からずエラーになるってワケです。
 
 
ちょっと説明が難しいですが、分かってもらえると助かります^^;
 
 
…で、どうすれば良いか?というと画像下の図のように

1. section:0に新しいセクションをインサート
2. 1によって”Category A”は”section:1に押し出される。
3. 空になった”Category B”のセクションを削除

 
こうすることで、移動前と移動後の矛盾はなくなるので意図した通りに表示が更新されるってワケです。

こう整理してみると簡単そうですが、実はここにも落とし穴があって
毎回同じ順番で同じ移動をするとは限りませんし、その状態によって当然移動の仕方も様々です。
(もっと複雑に移動する場合もあります。)
しかも、まったく同じ条件で移動をしたとしても、同じindexPathが飛んでくるとは限らないようなんです…(> <;)
 
 

3. 大規模な対策

これにはまったく参ってしまいました…
それで、かなり時間が経ってからある事に気付きました。

セクションの削除や挿入はリアルタイムで行わなくても良い!!
 
 
どういう事かと言うと、
セルの削除や挿入にしろ、セクションにしろ
[self.tableView beginUpdate]から[self.tableView endUpdate]までの間で処理すれば良い。
極端に言えば、[controller: didChangeObject…]では何もせずに最後に呼ばれる[controllerDidChangeContent:]内の[self.tableView endUpdate]の直前ですべての処理を行えば大丈夫!ってコトなんです。
 
 
そこで、僕はこの更新処理を別クラスを作って対応することにしました。
クラス名は”DTDSectionChecker”です。
(※今回はすべてのコードを載せますが、不完全なものなのでエラーが起こる事があります。)

//
//  DTDSectionChecker.h
//  doTodo
//
//  Created by Jacminik on 10/06/16.
//  Copyright 2010 EdenVision*. All rights reserved.
//

#import <Foundation/Foundation.h>


@interface DTDSectionChecker : NSObject {

	NSMutableArray *_checker;
	
	UITableView *_tableView;
	NSInteger _sectionState, _rowState;
	NSIndexPath *_selectedIndexPath;
	
	BOOL _hasEmptySection;
	NSUInteger _emptySection, _deletedSection, _insertedSection, _newInsertSection;
}

@property (nonatomic, retain) NSMutableArray *checker;
@property (nonatomic, retain) UITableView *tableView;
@property (nonatomic, assign) NSInteger sectionState, rowState;
@property (nonatomic, retain) NSIndexPath *selectedIndexPath;

@property (nonatomic, assign) BOOL hasEmptySection;
@property (nonatomic, assign) NSUInteger emptySection, deletedSection, insertedSection, newInsertSection;


- (id)initWithTableView:(UITableView *)tableView selectedIndexPath:(NSIndexPath *)selectedIndexPath;

- (void)setIndexPath:(NSIndexPath *)indexPath newIndexPath:(NSIndexPath *)newIndexPath;

- (void)changeMoveSection;

@end
//
//  DTDSectionChecker.m
//  doTodo
//
//  Created by Jacminik on 10/06/16.
//  Copyright 2010 EdenVision*. All rights reserved.
//

#import "DTDSectionChecker.h"


@implementation DTDSectionChecker

@synthesize checker = _checker, tableView = _tableView;
@synthesize sectionState = _sectionState, rowState = _rowState;
@synthesize selectedIndexPath = _selectedIndexPath;
@synthesize hasEmptySection = _hasEmptySection, emptySection = _emptySection;
@synthesize deletedSection = _deletedSection, insertedSection = _insertedSection;
@synthesize newInsertSection = _newInsertSection;


- (id)initWithTableView:(UITableView *)tableView selectedIndexPath:(NSIndexPath *)selectedIndexPath {
	
	if (self = [super init]) {
		
		self.tableView = tableView;
		self.selectedIndexPath = selectedIndexPath;
		LOG(@"selected: %@", _selectedIndexPath);
		
		self.emptySection = self.deletedSection = self.insertedSection = self.newInsertSection = -1;
	}
	return self;
}

// sectionIndexに対応するindexPath(NSMutableArray内のObjectIndex)を返す。
// 対応するオブジェクト(指定SectionのRowCountデータ)が無ければ"-1"を返す。
- (NSUInteger)indexOfSection:(NSUInteger)sectionIndex {
	
	NSUInteger index = -1;
	
	for (int i = 0; i < [_checker count]; i++) {
		NSIndexPath *indexPath = [_checker objectAtIndex:i];
		if (indexPath.section == sectionIndex)
			index = i;
	}

	return index;
}


// 移動後のSectionにRowCountを加算
- (void)changeRowCountInSection:(NSUInteger)sectionIndex isIncrement:(BOOL)increment {
	
	NSUInteger index = [self indexOfSection:sectionIndex];
	int numberOfRows;
	int addCount = (increment) ? 1 : -1;

	if (index == -1) {		// self.checkerに未登録のセクション
		numberOfRows = [_tableView numberOfRowsInSection:sectionIndex] + addCount;
		
		[_checker insertObject:[NSIndexPath indexPathForRow:numberOfRows inSection:sectionIndex]
					   atIndex:[_checker count]];
	}
	else {					// self.checkerに登録済みのセクション
		numberOfRows = ((NSIndexPath *)[_checker objectAtIndex:index]).row + addCount;
		
		[_checker replaceObjectAtIndex:index 
							withObject:[NSIndexPath indexPathForRow:numberOfRows inSection:sectionIndex]];
	}
	
	
	if (increment && _emptySection == sectionIndex) {	// newIndexPath(挿入先)が空になっていたセクションなら、
		self.hasEmptySection = NO;						// emptySectionを解除
		self.emptySection = -1;
		LOG(@"セクション:%d は、空ではなくなりました。", sectionIndex);
	}
	else if (!increment && numberOfRows == 0 && _deletedSection != sectionIndex) {
		self.hasEmptySection = YES;						// セクションが空になるならemptySectionとして記憶。
		self.emptySection = sectionIndex;
		LOG(@"セクション:%d は、空になりました。", sectionIndex);
	}
}


- (void)setIndexPath:(NSIndexPath *)indexPath newIndexPath:(NSIndexPath *)newIndexPath {
	
	LOG(@"from:(%d, %d), to:(%d, %d)", indexPath.section, indexPath.row, newIndexPath.section, newIndexPath.row);
	
	if (!_checker) {
		// tableViewの指定Section内の現在のRowCountを取得。
		int numberOfRows = [_tableView numberOfRowsInSection:indexPath.section];
		self.checker = [NSMutableArray arrayWithObject:[NSIndexPath indexPathForRow:numberOfRows 
																		  inSection:indexPath.section]];
		
		if (_rowState < 0)
			[self changeRowCountInSection:indexPath.section isIncrement:NO];
		
		if (_sectionState < 0 && _rowState > -2) 	// すでに削除されているセクションを登録
			self.deletedSection = (_selectedIndexPath) ? _selectedIndexPath.section : indexPath.section;
		else if (_sectionState > 0 && indexPath == _selectedIndexPath)
			self.insertedSection = newIndexPath.section;
	}
	
	
	[self changeRowCountInSection:indexPath.section isIncrement:NO];
	if (indexPath == newIndexPath && _sectionState > 0) // indexが同じ = セクションはインサート済み
		_insertedSection = indexPath.section;
	else					
		[self changeRowCountInSection:newIndexPath.section isIncrement:YES];
	
	
	
	// selectedIndexと違うセクションのオブジェクトの移動先をnewInsertSectionとして記憶
	if (_selectedIndexPath.section != indexPath.section 
		&& newIndexPath.section != _selectedIndexPath.section && indexPath.section != newIndexPath.section)
		self.newInsertSection = newIndexPath.section;
	
	if (_selectedIndexPath != indexPath && _selectedIndexPath.section == indexPath.section
		&& indexPath.section != newIndexPath.section) {
		self.newInsertSection = newIndexPath.section;
	}
//	if (_selectedIndexPath == indexPath)
//		self.newInsertSection = newIndexPath.section;
}

// self.checkerの中から、Rowが空/または入っているセクションを配列で返す。
- (NSMutableArray *)sectionsWithEmpty:(BOOL)isEmpty {
	
	NSMutableArray *sections = [NSMutableArray array];
	
	if (isEmpty) {
		for (NSIndexPath *indexPath in _checker) {
			if (indexPath.row == 0)
				[sections addObject:[NSNumber numberWithInt:indexPath.section]];
		}
	}
	else {
		for (NSIndexPath *indexPath in _checker) {
			if (indexPath.row != 0)
				[sections addObject:[NSNumber numberWithInt:indexPath.section]];
		}
	}
	
	return sections; 
}


- (void)changeMoveSection {
	
	LOG(@"rowState:%d, secState:%d", _rowState, _sectionState);
	LOG(@"deleted Sectin:%d", _deletedSection);
	LOG(@"inserted Sectin:%d", _insertedSection);
	LOG(@"newInserted Sectin:%d", _newInsertSection);
	
	if (!_hasEmptySection)
		return;
	if ((!_hasEmptySection && _sectionState < 1) || (_rowState == 1 && _sectionState == 1)
		|| (_insertedSection != -1 && !_sectionState && !_hasEmptySection))
		return;

	// Sectionの削除処理
	NSMutableArray *sections = [self sectionsWithEmpty:YES];
	NSNumber *sectionNumber;
	int deleteCount = 0;
	
	UITableViewRowAnimation fade = UITableViewRowAnimationFade;

	if ([sections count]) {
		for (sectionNumber in sections) {
			
			NSUInteger section = [sectionNumber intValue];
			
			if (section != _deletedSection) {
				LOG(@"DeleteBlock #0/Section:%d", section);
				[_tableView deleteSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:fade];
				deleteCount++;
			}
		}
	}
	else {
		if (_emptySection != -1)  {		// Category編集Viewで削除された場合に呼ばれる。
			LOG(@"DeleteBlock #1/Section:%d", _emptySection);
			[_tableView deleteSections:[NSIndexSet indexSetWithIndex:_emptySection] 
					  withRowAnimation:fade];
		}
		else if (_insertedSection) {	// 既にインサートされているセクションがある場合。
			LOG(@"DeleteBlock #2/Section:%d", _insertedSection);
			[_tableView deleteSections:[NSIndexSet indexSetWithIndex:_insertedSection] 
					  withRowAnimation:fade];
		}
		deleteCount++;
	}

	
	// Sectionのインサート処理
	sections = [self sectionsWithEmpty:NO];
	
	if ([sections count] == 2) {
		if (deleteCount == 1) {
			if (_newInsertSection != -1) {
				LOG(@"InsertBlock #0/Section:%d", _newInsertSection);
				[_tableView insertSections:[NSIndexSet indexSetWithIndex:_newInsertSection] 
						  withRowAnimation:fade];
			}
			else {
				for (sectionNumber in sections) {
					
					NSUInteger section = [sectionNumber intValue];
					
					if (section != _insertedSection && section != _selectedIndexPath.section) {
						LOG(@"InsertBlock #1/Section:%d", section);
						[_tableView insertSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:fade];
					}
				}
			}
		}
		else {
			for (sectionNumber in sections) {
				NSUInteger section = [sectionNumber intValue];
				if (section == _selectedIndexPath.section || section == _insertedSection){
				}
				else {
					LOG(@"InsertBlock #2/Section:%d", [sectionNumber intValue]);
					[_tableView insertSections:[NSIndexSet indexSetWithIndex:section] 
								  withRowAnimation:fade];
				}			
			}
		}
	}
	else {
		LOG(@"InsertBlock #3/Section:%d", [[sections lastObject] intValue]);
		[_tableView insertSections:[NSIndexSet indexSetWithIndex:[[sections lastObject] intValue]] 
					  withRowAnimation:fade];
	}
}

- (void)dealloc {
	self.selectedIndexPath = nil;
	self.checker = nil;
	self.tableView = nil;
	
	[super dealloc];
}

@end

今後みなさんが作る実際のアプリでこのようなクラスが必要になる事は、滅多に無いと思うので詳しくは説明しませんが、どんな事をやっているのかは説明しておきますね。
(まるまるコピーしてください。)
 
 
このクラスでは、
セルやセクションの削除/挿入の状態やその増減などを把握させて
どのセクションが空になったのか?
セクションを挿入する必要があるか?
などを判断して、最終的に必要があるなら実行します。

ただ、この処理の分岐の中にまだ抜け道がありエラーも発生します。
なので、やはり本来はデータの構造を考えた方がパフォーマンス的にも絶対的に良いと思います。
 
 

4. SectionCheckerを使う。

では、このクラスを使うための実装をしていきましょう。

まずRootViewControlle.hに以下を加えます。

// クラスの宣言
@class DTDSectionChecker;

// インスタンス変数宣言
DTDSectionChecker *_sectionChecker;
NSIndexPath *_selectedIndexPath;

// プロパティ
@property (nonatomic, retain) DTDSectionChecker *sectionChecker;
@property (nonatomic, retain) NSIndexPath *selectedIndexPath;

 
で、RootViewController.mには以下を。

// インポート
#import "DTDSectionChecker.h"

// sythesize
@synthesize sectionChecker = _sectionChecker;
@synthesize selectedIndexPath = _selectedIndexPath;

 
 
次に[didSelectRowAtIndexPath:]でself.selectedIndexPathへ代入します。

#pragma mark -
#pragma mark Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    
	_masterTask = [fetchedResultsController_ objectAtIndexPath:indexPath];
	
	self.selectedIndexPath = indexPath;
	
	DetailViewController *detailViewController = [[DetailViewController alloc] 
												  initWithStyle:UITableViewStyleGrouped 
												  editableObject:[self addNewTask:_masterTask]];
	detailViewController.title = @"Task Detail";
	detailViewController.delegate = self;
	
	[self.navigationController pushViewController:detailViewController animated:YES];
	[detailViewController release];
}

 
そしたら肝心の四つのメソッドへ


#pragma mark -
#pragma mark Fetched results controller delegate

// rowAnimationを設定
const UITableViewRowAnimation fade = UITableViewRowAnimationFade;

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
	
	// Sectionの移動を管理するオブジェクトを用意
	DTDSectionChecker *provChecker = [[DTDSectionChecker alloc] initWithTableView:self.tableView 
																selectedIndexPath:_selectedIndexPath];
	self.sectionChecker = provChecker;
	[provChecker release];
	
	self.selectedIndexPath = nil;
}

 

- (void)controller:(NSFetchedResultsController *)controller 
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:fade];
			_sectionChecker.sectionState++;
			_sectionChecker.insertedSection = sectionIndex;
            break;
            
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:fade];
			_sectionChecker.sectionState--;
			_sectionChecker.deletedSection = sectionIndex;
            break;
    }
}

 

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath {
    
    UITableView *tableView = self.tableView;
    
    switch(type) {
            
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:fade];
			_sectionChecker.rowState++;
            break;
            
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:fade];
			_sectionChecker.rowState--;
            break;
            
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
            
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:fade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:fade];
			[_sectionChecker setIndexPath:indexPath newIndexPath:newIndexPath];
            break;
    }
}

 

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    
	[_sectionChecker changeMoveSection];
	self.sectionChecker = nil;
	
	UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:nil message:nil delegate:self 
											   cancelButtonTitle:@"OK" otherButtonTitles:nil, nil] autorelease];
	@try {
		[self.tableView endUpdates];
	}
	@catch (NSException *exception) {
		LOG(@"Error name: %@, reason: %@", [exception name], [exception reason]);
		[alertView setTitle:@"エラーが発生しました。"];
		[alertView setMessage:@"一旦アプリを終了し、再起動してください。"];
		[alertView show];
	}
	@finally {
		
	}
}

 
最後に[fetchedResultsController]の初期化時のキャッシュをnilにしておきます。

 NSFetchedResultsController *aFetchedResultsController = 
	[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest 
										managedObjectContext:self.managedObjectContext
										  sectionNameKeyPath:@"category.name" cacheName:nil];

 
これらを実装したら、一旦アプリを削除した後にビルドして確認してみてくださいね。
 
 
これである程度、エラーを減らす事は出来ましたが
それでも頻繁にカテゴリーの変更などを繰り返してみると致命的なエラーになることがあります。

NSFetchedResultsController ERROR: The fetched object at index 3 has an out of order section name ‘XXXXXX. Objects must be sorted by section name’

Error performing fetch: The operation couldn’t be completed. (Cocoa error 134060.)

 
つまり、ソートするためのsection nameが壊れてしまう事があるようです。
これは、色々変更した後にビルドを終了してから再度ビルドしようとすると発生するんですが、
このエラーが出ると一旦アプリを削除しない限り、まったく起動しなくなってしまいます。
 
 
このdotodoはテストプロジェクトだからまだいいものの、これが実際にリリース用のアプリだったらかなりマズいエラーですよね(> , <;)

なので、しつこいようですがセクションとソートについては特に
無理のない安全な設計を心がけることはCoreDataを使ったアプリを作る上でかなり重要なポイントになるところだと思います。
 
くれぐれも今回の僕の二の舞にならないように気を付けでくださいね(苦笑)
 
 
 
それでは、明日はスッキリ気持ちを入れ替えて『検索機能の実装』に入っていきます!
 
 
ちょっとした小技も紹介しますのでお見逃しなくっ!!
 

広告

Study CoreData 19 ~最悪のシナリオ~」への1件のフィードバック

  1. ピンバック: [朝刊] アプリ開発者のiAd広告出稿が可能になったそうです。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中