📜 ⬆️ ⬇️

Another ActiveRecord implementation on Objective-C

I want to share another implementation of the ActiveRecord pattern on Objective-C, specifically for iOS.

When I first started using CoreData in iOS development, even then there appeared thoughts that this interaction could be somehow simplified. After some time, I became acquainted with ActiveRecord from RubyOnRails, and then I understood what was missing for me.
A little search on the githabe found a lot of implementations, but for various reasons I did not like them. Some are written for CoreData, but I don’t like it, in others you need to create tables with your hands, or write raw sql queries. And in some, the code was obscenely awful, and I myself sometimes write is not very clean, but the huge fence from the nested if / switch / if / switch is too much.
In the end I decided to write my own bike, without CoreData and without SQL for the user.
The main reason for this development was, is and, I hope, will be - interest in the development.

That's what came of it all.
And under the cat a small description of the possibilities and implementation (in fact, a lot of text and pieces of code, a summary at the very end of the article).

Creating tables


The first problem was the creation of tables.
In the case of CoreData, you don’t need to create anything, you just need to describe the entities, and the CD will do the rest.
I have long thought about how it would be better to issue, and after a while it dawned on me.
Objective-C allows you to get a list of all subclasses for any class, and also get a list of all its properties. Thus, the description of the entity will be a simple description of the class, and all I have to do is collect this information and build an SQL query based on it.
Entity description
')
@interface User : ActiveRecord @property (nonatomic, retain) NSString *name; @end 

Getting all subclasses

 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; } 

Getting all the properties down to the base class

 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); } 


Data types


For more flexibility, we had to abandon the basic data types (int, double, etc.) and work only with classes as table fields.
Thus, any class can be used as a table field; the only requirement is that it should be able to save and load itself.
For this, it must implement ARRepresentationProtocol

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

I implemented these methods for base types of Foundation framework using categories
- NSDecimalNumber - real
- NSNumber - integer
- NSString - text
- NSData - blob
- NSDate - date (real)
but the set of these classes can be expanded at any time, without much difficulty.

CoreData allows to achieve the same with the data type Transformable, but I still have not mastered how to work with it.

CRUD for records


Create

The process of creating a new entry is very simple and transparent.

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

Read

Getting all the records

 NSArray *users = [User allRecords]; 

Often, all the entries are not needed, so I added the implementation of filters, but about them later.

Update

 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 monitors the change of all properties, and when updated creates a request only to update the changed fields.

Delete

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

All records have the id property (NSNumber), which is removed.

Waste fields


How to deal with the fields that we do not need to save to the database? Just ignore them :)
To do this, in the class implementation you need to add the following construction, this is a simple macro macro.

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


Validation


One of the requirements that I set for myself in development is support for validations.
Currently there are two types of validation implemented: for presence and for uniqueness.
The syntax is simple, and also uses macro macros. In addition, the class must implement ARValidatableProtocol, nothing is required of the user, this is done in order not to run the validation mechanism for classes that do not use it.

 // 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 

In addition, I implemented support for custom validators that the user can add.
To do this, you need to create a class validator, which must implement the ARValidatorProtocol and describe it in a validated class.
ARValidatorProtocol

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

Custom validator

 // 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 

Error processing


The save, update and isValid methods return boolean values; if false / NO returns, you can get a list of errors

 [user errors]; 

then an array of ARError objects will be returned

 @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 

This class does not contain any detailed error messages, but only keywords, on the basis of which you can create a localized message and display it to the application user.

Migrations


Migrations are implemented at a primitive level: it responds only to the addition of new fields to entities or to the addition of new entities.
To use migrations, you do not need to write anything anywhere.
When you first start the application, all tables are created, and on subsequent launches, it checks whether there are new table fields or, if they exist, then alter table requests are made.
In order not to instantiate checking for a change in the structure of tables, you should send the following message before any calls to ActiveRecord

 [ActiveRecord disableMigrations]; 

Transactions


I also implemented the ability to use transactions, blocks are used for this

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

rollback is an ordinary macro that throws an ARException type exception.
You can use tarnzczytsii not only to roll back in case of failure, but also to increase the speed of query execution when adding records.
In one of the projects there was a terrible brake when trying to create over9000 records. The execution time for the dump was about 180 seconds, after I wrapped it in a BEGIN transaction; ... COMMIT; time dropped to ~ 4-5 seconds. So I advise everyone who does not know.

Connections


When I became acquainted with the implementation of ActiveRecord in RoR, I was fascinated by the simplicity of creating connectivity between entities. By and large, this simplicity was the first prerequisite for the creation of this framework. And now I think the most important feature in my bike is just the connection between the entities, and their relative simplicity.
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 


The macros belongs_to_dec belonsg_to_imp take three parameters: the name of the class with which we “associate”, the name of getter and the type of dependency.
There are two types of dependencies: ARDependencyNullify and ARDependencyDestroy, the first when removing a model, removes its connections, and the second deletes all related entities.
The field for this relationship must match the model name and begin with a lowercase letter.
Group <-> groupId
User <-> userId
ContentManager <-> contentManagerId
EMCategory <-> eMCategory // a little clumsy, but historically

Feedback (HasMany)

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

The same as in the case of a BelongsTo connection.
The main thing you need to remember: before creating a connection, both records must be saved, otherwise they do not have an id, and the connections are tied to it.

HasManyThrough


To create this connection, you need to create another model, an intermediate one.

 // 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 

Intermediate binding model

 // 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 

This connection has the same disadvantages as HasMany.

* _Dec / * _ imp macros add helper methods to add links
 set#ModelName:(ActiveRecord *)aRecord; // BelongsTo add##ModelName:(ActiveRecord *)aRecord; // HasMany, HasManyThrough remove##ModelName:(ActiveRecord *)aRecord; // HasMany, HasManyThrough 

Filters for queries


It is often necessary to somehow filter the sample from the database:
- search of records corresponding to some kind of template (UISearchBar)
- output to the table only 5 entries out of a thousand
- getting only text fields of records, without getting heaps of “heavy” images from the base
- A lot of options :)

At first, I also did not know how to implement all this in a convenient form, but then again I remembered Ruby and its inherent "laziness", in the end I decided to create a class that would get records only on demand, but accept filters in any order.
That's what came out of it.

Limit / Offset

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

Only / Except

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

Where

iActiveRecord supports basic WHERE conditions

 - (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, ...; 

The same can be described in the style of familiar and convenient NSPredicates.

 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]; 

Join


Itself almost never used it, but decided that for completeness it is necessary to implement.

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

Different types of joines are supported
- ARJoinLeft
- ARJoinRight
- ARJoinInner
- ARJoinOuter
I think the names speak for themselves.
One small crutch is associated with this opportunity, so to get merged records you need to call

 - (NSArray *)fetchJoinedRecords; 

instead

 - (NSArray *)fetchRecords; 

This method returns an array of dictionaries, where the keys are the names of the entities, and the values ​​are the data from the database.

Sorting


 - (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]; 


Storage


The base can be stored both in Caches and in Documents, while in the case of storage in Documents the attribute that turns off the backup is added to the file

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

otherwise, the application will receive a reject from Apple.

Summary


Project on github - iActiveRecord .
Opportunities


Conclusion



As an excuse for the conclusion, I want to say that the project started just for fun, and it continues to evolve, in the plans, eventually, to clean up a bunch of dirty code and add other useful features.

I am pleased to hear adequate criticism.

PS error messages write to the LAN, please.

Source: https://habr.com/ru/post/145108/


All Articles