Objective-Cの別のActiveRecord実装

Objective-C、特にiOS用のActiveRecordパターンの別の実装を共有したいと思います。



iOS開発でCoreDataを使用し始めたばかりのときでも、この相互作用は何らかの形で単純化できると考えられていました。 しばらくしてから、RubyOnRailsのActiveRecordに会ったところ、何が欠けているのかがわかりました。

githubを少し検索して、多くの実装を見つけましたが、さまざまな理由でそれらが好きではありませんでした。 CoreData用に作成されたものもありますが、私は好きではありません。他のものでは、手でテーブルを作成するか、生のSQLクエリを作成する必要があります。 また、場合によってはコードがひどくひどかったので、私は時々自分であまりきれいに書かないことがありますが、囲まれたif / switch / if / switchからの巨大なフェンスが多すぎます。

最終的に、CoreDataとユーザーのSQLを使用せずに自転車を作成することにしました。

この開発の主な理由は、開発に興味があることです。



これがすべての結果です。

そして、猫の下には、可能性と実装についての小さな説明があります(実際、多くのテキストとコード、記事の最後の要約)。



テーブル作成



最初の問題はテーブルの作成でした。

CoreDataの場合、何も作成する必要はなく、エンティティを記述するだけで、あとはCDが処理します。

私はそれをより良く整理する方法について長い間考えていましたが、しばらくするとそれが私に気付きました。

Objective-Cでは、任意のクラスのすべてのサブクラスのリストを取得でき、さらにそのすべてのプロパティのリストを取得できます。 したがって、エンティティの説明はクラスの簡単な説明になります。この情報を収集し、それに基づいてSQLクエリを作成するだけです。

エンティティの説明



@interface User : ActiveRecord @property (nonatomic, retain) NSString *name; @end
      
      





すべてのサブクラスを取得する



 static NSArray *class_getSubclasses(Class parentClass) { int numClasses = objc_getClassList(NULL, 0); Class *classes = NULL; classes = malloc(sizeof(Class) * numClasses); numClasses = objc_getClassList(classes, numClasses); NSMutableArray *result = [NSMutableArray array]; for (NSInteger i = 0; i < numClasses; i++) { Class superClass = classes[i]; do{ superClass = class_getSuperclass(superClass); } while(superClass && superClass != parentClass); if (superClass == nil) { continue; } [result addObject:classes[i]]; } return result; }
      
      





すべてのプロパティを基本クラスまで取得する



 Class BaseClass = NSClassFromString(@"NSObject"); id CurrentClass = aRecordClass; while(nil != CurrentClass && CurrentClass != BaseClass){ unsigned int outCount, i; objc_property_t *properties = class_copyPropertyList(CurrentClass, &outCount); for (i = 0; i < outCount; i++) { // do something with concrete property => properties[i] } CurrentClass = class_getSuperclass(CurrentClass); }
      
      







データ型



柔軟性を高めるために、基本データ型(int、doubleなど)を放棄し、クラスをテーブルフィールドとしてのみ使用する必要がありました。

したがって、任意のクラスをテーブルフィールドとして使用できます。唯一の要件は、それ自体を保存およびロードできる必要があることです。

これを行うには、ARRepresentationProtocolを実装する必要があります



 @protocol ARRepresentationProtocol @required + (const char *)sqlType; - (NSString *)toSql; + (id)fromSql:(NSString *)sqlData; @end
      
      





カテゴリを使用してFoundationフレームワークタイプにこれらのメソッドを実装しました

-NSDecimalNumber-実数

-NSNumber-整数

-NSString-テキスト

-NSData-ブロブ

-NSDate-日付(実数)

ただし、これらのクラスのセットはいつでも拡張でき、それほど難しくありません。



Transformableデータ型のCoreDataを使用しても同じことが実現できますが、それをどのように使用するかはまだわかりません。



レコードのCRUD



作成する


新しいレコードを作成するプロセスは非常にシンプルで透過的です。



 User *user = [User newRecord]; user.name = @"Alex"; [user save];
      
      





読む


すべてのレコードを取得する



 NSArray *users = [User allRecords];
      
      





多くの場合、すべてのレコードが必要ではないため、フィルターの実装を追加しましたが、それらについては後で詳しく説明します。



更新する


 User *user = [User newRecord]; user.name = @"Alex"; [user save]; NSArray *users = [User allRecords]; User *userForUpdate = [users first]; userForUpdate.name = @"John"; [userForUpdate update]; //  [userForUpdate save];
      
      





ActiveRecordはすべてのプロパティの変更を監視し、更新時には変更されたフィールドのみを更新するリクエストを作成します。



削除する


 NSArray *users = [User allRecords]; User *userForRemove = [users first]; [userForRemove dropRecord];
      
      





すべてのレコードには、削除に使用されるid(NSNumber)プロパティがあります。



不要なフィールド



データベースに保存する必要のないフィールドはどうですか? 無視してください:)

これを行うには、クラスの実装で次の構成を追加する必要があります。これは単純なマクロシステムです。



 @implementation User ... @synthesize ignoredProperty; ... ignore_fields_do( ignore_field(ignoredProperty) ) ... @end
      
      







検証



開発で自分自身に設定した要求の1つは、検証のサポートです。

現在、可用性と一意性の2種類の検証が実装されています。

構文は単純で、colマクロも使用します。 さらに、クラスはARValidatableProtocolを実装する必要があります。ユーザーからの要求はありません。これは、それを使用しないクラスの検証メカニズムを開始しないようにするためです。



 // User.h @interface User : ActiveRecord <ARValidatableProtocol> ... @property (nonatomic, copy) NSString *name; ... @end // User.m @implementation User ... validation_do( validate_uniqueness_of(name) validate_presence_of(name) ) ... @end
      
      





さらに、ユーザー自身が追加できるカスタムバリデーターのサポートを実装しました。

これを行うには、ARValidatorProtocolを実装し、検証済みのクラスで記述する必要がある検証クラスを作成する必要があります。

ARValidatorProtocol



 @protocol ARValidatorProtocol <NSObject> @optional - (NSString *)errorMessage; @required - (BOOL)validateField:(NSString *)aField ofRecord:(id)aRecord; @end
      
      





カスタム検証



 // PrefixValidator.h @interface PrefixValidator : NSObject <ARValidatorProtocol> @end // PrefixValidator.m @implementation PrefixValidator - (NSString *)errorMessage { return @"Invalid prefix"; } - (BOOL)validateField:(NSString *)aField ofRecord:(id)aRecord { NSString *aValue = [aRecord valueForKey:aField]; BOOL valid = [aValue hasPrefix:@"LOL"]; return valid; } @end
      
      





エラー処理



save、update、isValidメソッドはブール値を返します; false / NOが返された場合、エラーのリストを取得できます



 [user errors];
      
      





その後、クラスARErrorのオブジェクトの配列が返されます



 @interface ARError : NSObject @property (nonatomic, copy) NSString *modelName; @property (nonatomic, copy) NSString *propertyName; @property (nonatomic, copy) NSString *errorName; - (id)initWithModel:(NSString *)aModel property:(NSString *)aProperty error:(NSString *)anError; @end
      
      





このクラスには詳細なエラーメッセージは含まれませんが、ローカライズされたメッセージを作成してアプリケーションユーザーに表示できるキーワードのみが含まれます。



移行



移行はプリミティブレベルで実装されます。エンティティへの新しいフィールドの追加または新しいエンティティの追加にのみ応答します。

移行を使用するために、どこにも登録する必要はありません。

アプリケーションを初めて起動すると、すべてのテーブルが作成され、その後の起動時に、新しいフィールドまたはテーブルが存在するかどうかが確認され、存在する場合は、テーブルクエリが変更されます。

テーブル構造の変更のチェックをインスタンス化しないために、ActiveRecordを呼び出す前に次のメッセージを送信する必要があります



 [ActiveRecord disableMigrations];
      
      





取引



トランザクションを使用する機能も実装しました。これにはブロックが使用されます



 [ActiveRecord transaction:^{ User *alex = [User newRecord]; alex.name = @"Alex"; [alex save]; rollback }];
      
      





rollback-タイプARExceptionの例外をスローする通常のマクロ。

Tarnzaktsiiは、障害が発生した場合のロールバックだけでなく、レコードを追加する際のクエリ実行の速度を上げるためにも使用できます。

プロジェクトの1つは、9000以上のレコードを作成しようとしたときにひどいブレーキをかけました。 ダンプの実行時間は、BEGINトランザクションでラップした後、約180秒でした; ... COMMIT; 時間は約4〜5秒に短縮されました。 だから、私は知らない人にアドバイスします。



コミュニケーションズ



RoRでのActiveRecordの実装に精通したとき、エンティティ間の関係を簡単に作成できることに満足しました。 概して、このシンプルさは、このフレームワークを作成するための最初の前提条件となりました。 そして今、私は自転車の最も重要な機能は、エンティティ間の接続とそれらの相対的なシンプルさであると考えています。

HasMany <-> BelongsTo


 // User.h @interface User : ActiveRecord ... @property (nonatomic, retain) NSNumber *groupId; ... belongs_to_dec(Group, group, ARDependencyNullify) ... @end // User.m @implementation User ... @synthesize groupId; ... belonsg_to_imp(Group, group, ARDependencyNullify) ... @end
      
      







belongs_to_dec belonsg_to_impマクロは3つのパラメーターを受け入れます:「コンタクト」しているクラスの名前、ゲッターの名前、依存関係のタイプ。

依存関係には、ARDependencyNullifyとARDependencyDestroyの2種類があります。1つ目はモデルを削除すると関係が無効になり、2つ目は関連するすべてのエンティティが削除されます。

この関係のフィールドはモデル名と一致し、小文字で始まる必要があります

グループ<-> groupId

ユーザー<-> userId

ContentManager <-> contentManagerId

EMCategory <-> eMCategory //少し不器用ですが、歴史的には



フィードバック(HasMany)



 // Group.h @interface Group : ActiveRecord ... has_many_dec(User, users, ARDependencyDestroy) ... @end // Group.m @implementation Group ... has_many_imp(User, users, ARDependencyDestroy) ... @end
      
      





BelongsToタイプの通信と同じです。

覚えておくべき主なこと:リンクを作成する前に、両方のレコードを保存する必要があります。保存しないと、IDがなく、リンクがそれに関連付けられます。



HasManyThrough



この接続を作成するには、別のモデル、中間モデルを作成する必要があります。



 // User.h @interface User : ActiveRecord ... has_many_through_dec(Project, UserProjectRelationship, projects, ARDependencyNullify) ... @end // User.m @implementation User ... has_many_through_imp(Project, UserProjectRelationship, projects, ARDependencyNullify) ... @end // Project.h @interface Project : ActiveRecord ... has_many_through_dec(User, UserProjectRelationship, users, ARDependencyDestroy) ... @end // Project.m @implementation Project ... has_many_through_imp(User, UserProjectRelationship, users, ARDependencyDestroy) ... @end
      
      





中間バインディングモデル



 // UserProjectRelationship.h @interface UserProjectRelationship : ActiveRecord @property (nonatomic, retain) NSNumber *userId; @property (nonatomic, retain) NSNumber *projectId; @end // UserProjectRelationship.m @implementation UserProjectRelationship @synthesize userId; @synthesize projectId; @end
      
      





この接続には、HasManyと同じ欠点があります。



* _Dec / * _ impマクロはヘルパーメソッドを追加してリンクを追加します

 set#ModelName:(ActiveRecord *)aRecord; // BelongsTo add##ModelName:(ActiveRecord *)aRecord; // HasMany, HasManyThrough remove##ModelName:(ActiveRecord *)aRecord; // HasMany, HasManyThrough
      
      





クエリフィルター



非常に頻繁に、データベースからの選択を何らかの方法でフィルタリングする必要があります。

-あるテンプレートに対応するレコードの検索(UISearchBar)

-1000のうち5レコードのみをテーブルに出力

-「重い」写真のデータベースヒープから取得せずに、レコードのテキストフィールドのみを受信する

-まだ多くのオプション:)



最初は、これらすべてを便利な形式で実装する方法も考えられませんでしたが、Rubyとその固有の「遅延」を思い出し、最終的に、要求に応じてのみレコードを取得するが、任意の順序でフィルターを受け入れるクラスを作成することにしました。

これがその結果です。



制限/オフセット


 NSArray *users = [[[User lazyFetcher] limit:5] fetchRecords]; NSArray *users = [[[User lazyFetcher] offset:5] fetchRecords]; NSArray *users = [[[[User lazyFetcher] offset:5] limit:2] fetchRecords];
      
      





のみ/を除く


 ARLazyFetcher *fetcher = [[User lazyFetcher] only:@"name", @"id", nil]; ARLazyFetcher *fetcher = [[User lazyFetcher] except:@"veryBigImage", nil];
      
      





どこで


iActiveRecordはWHERE基本条件をサポートします



 - (ARLazyFetcher *)whereField:(NSString *)aField equalToValue:(id)aValue; - (ARLazyFetcher *)whereField:(NSString *)aField notEqualToValue:(id)aValue; - (ARLazyFetcher *)whereField:(NSString *)aField in:(NSArray *)aValues; - (ARLazyFetcher *)whereField:(NSString *)aField notIn:(NSArray *)aValues; - (ARLazyFetcher *)whereField:(NSString *)aField like:(NSString *)aPattern; - (ARLazyFetcher *)whereField:(NSString *)aField notLike:(NSString *)aPattern; - (ARLazyFetcher *)whereField:(NSString *)aField between:(id)startValue and:(id)endValue; - (ARLazyFetcher *)where:(NSString *)aFormat, ...;
      
      





同じことがおなじみの便利なNSPredicate'ovのスタイルで説明できます。



 NSArray *ids = [NSArray arrayWithObjects: [NSNumber numberWithInt:1], [NSNumber numberWithInt:15], nil]; NSString *username = @"john"; ARLazyFetcher *fetcher = [User lazyFetcher]; [fetcher where:@"'user'.'name' = %@ or 'user'.'id' in %@", username, ids, nil]; NSArray *records = [fetcher fetchRecords];
      
      





参加する



私はこれを自分で使用したことはほとんどありませんでしたが、完全を期すために実装する必要があると判断しました。



 - (ARLazyFetcher *)join:(Class)aJoinRecord useJoin:(ARJoinType)aJoinType onField:(NSString *)aFirstField andField:(NSString *)aSecondField;
      
      





さまざまなタイプの結合がサポートされています

-ARJoinLeft

-ARJoinRight

-ARJoinInner

-ARJoinOuter

名前はそれ自体を物語っていると思います。

1つの小さな松葉杖がこの機能に関連付けられているため、結合レコードを取得するには、呼び出す必要があります



 - (NSArray *)fetchJoinedRecords;
      
      





の代わりに



 - (NSArray *)fetchRecords;
      
      





このメソッドは、キーがエンティティ名であり、値がデータベースからのデータである辞書から配列を返します。



仕分け



 - (ARLazyFetcher *)orderBy:(NSString *)aField ascending:(BOOL)isAscending; - (ARLazyFetcher *)orderBy:(NSString *)aField;// ASC   ARLazyFetcher *fetcher = [[[User lazyFetcher] offset:2] limit:10]; [[fetcher whereField:@"name" equalToValue:@"Alex"] orderBy:@"name"]; NSArray *users = [fetcher fetchRecords];
      
      







保管



データベースはキャッシュとドキュメントの両方に保存できます。ドキュメントに保存する場合、バックアップを切断する属性がファイルに追加されます



 u_int8_t b = 1; setxattr([[url path] fileSystemRepresentation], "com.apple.MobileBackup", &b, 1, 0, 0);
      
      





そうでない場合、アプリケーションはAppleから拒否を受け取ります。



まとめ



githubプロジェクトはiActiveRecordです。

特徴





おわりに





結論を正当化するために 、私はプロジェクトがただの楽しみのために始まったと言いたいと思います、そして、それは最終的にたくさんの「汚い」コードをきれいにし、他の有用な機能を追加する計画で、開発を続けています。



適切な批判を聞いてうれしいです。



PSはLANにエラーメッセージを書き込みます。



All Articles