Another ActiveRecord implementation on Objective-C
I want to share another implementation of the ActiveRecord pattern on Objective-C, and specifically for iOS.
When I was just starting to use CoreData in iOS development, even then there were thoughts that this interaction could be somehow simplified. After some time, I met with ActiveRecord from RubyOnRails, and then I realized what I was missing.
Having searched a little on the github I found a lot of implementations, but for various reasons I did not like them. Some are written for CoreData, but I do not like it, in others you need to create tables with your hands, or write raw sql queries. And in some cases the code was terribly terrible, sometimes I’m not writing it very cleanly myself, but the huge fence from the enclosed if / switch / if / switch is too much.
In the end, I decided to write my 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.
Here is what came of it all.
And under the cat there is a small description of the possibilities and implementation (in fact, there is a lot of text and pieces of code, a summary at the very end of the article).
The first problem was creating 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 thought for a long time about how to arrange it better, and after a while it dawned on me.
Objective-C allows you to get a list of all subclasses for any class, and in addition to get a list of all its properties. Thus, the description of the entity will be a simple description of the class, and all that remains for me to do is to collect this information and compose an SQL query based on it.
Entity description
Getting all subclasses
Getting all properties down to the base class
For greater flexibility, I 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: it must be able to save and load itself.
To do this, it must implement ARRepresentationProtocol
I implemented these methods for the Foundation framework types using the 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 with the Transformable data type allows you to achieve the same thing, but I still have not figured out how to work with it.
The process of creating a new record is very simple and transparent.
Retrieving All Records
Often, all the records are not needed, so I added an implementation of filters, but more about them later.
ActiveRecord monitors changes in all properties, and when updating creates a request only to update the changed fields.
All records have the id (NSNumber) property, which is used for deletion.
What about fields that we don’t need to save to the database? Just ignore them :)
To do this, add the following construct to the class implementation, this is a simple macro macro.
One of the demands that I set for myself in development is the support of validations.
At the moment, two types of validation are implemented: for availability and for uniqueness.
The syntax is simple, and also uses col macros. In addition, the class must implement ARValidatableProtocol, nothing is required from the user, this is done so as not to start the validation mechanism for classes that do not use it.
In addition, I implemented support for custom validators that the user himself can add.
To do this, you need to create a validator class that must implement ARValidatorProtocol and describe it in a validated class.
ARValidatorProtocol
Custom validator
The save, update, and isValid methods return boolean values; if false / NO is returned, you can get a list of errors
after which an array of objects of class ARError will be returned
This class does not contain any detailed error messages, but only keywords based on which you can create a localized message and display it to the application user.
Migrations are implemented at a primitive level: it only responds to adding new fields to entities or to adding new entities.
To use migrations, you don’t need to register anything anywhere.
At the first launch of the application, all tables are created, and at subsequent launches, the table is checked for new fields or if there are any, then alter table queries are made.
In order not to instantiate a check for changes in the table structures, you must send the following message before any calls to ActiveRecord
I also implemented the ability to use transactions, blocks are used for this
rollback - an ordinary macro that throws an exception of type ARException.
Tarnzaktsii can be used not only for rollback in case of failure, but also to increase the speed of query execution when adding records.
One of the projects had a terrible brake when trying to create over9000 records. The dump execution time was about 180 seconds after I wrapped it in a BEGIN transaction; ... COMMIT; time decreased to ~ 4-5 seconds. So I advise everyone who is not in the know.
When I became acquainted with the implementation of ActiveRecord in RoR, I was delighted with the simplicity of creating a connection between entities. By and large, this simplicity served as the first prerequisite for the creation of this framework. And now I consider the most important feature in my bike to be just the connections between the entities, and their relative simplicity.
The belongs_to_dec belonsg_to_imp macros accept three parameters: the name of the class we are “contacting” with, the name of the getter, and the type of dependency.
There are two types of dependencies: ARDependencyNullify and ARDependencyDestroy, the first when deleting the model nullifies its relationships, and the second removes all related entities.
The field for this relationship should 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 )
The same as with BelongsTo type communications.
The main thing to remember: before creating a link, both records must be saved, otherwise they do not have an id, and the links are tied to it.
To create this connection, you need to create another model, an intermediate one.
Intermediate binding model
This association has the same drawbacks as HasMany.
Macros * _dec / * _ imp add helper methods to add associations.
Very often, it is required to somehow filter the selection from the database:
- search for records matching some kind of template (UISearchBar)
- display in the table only 5 records out of a thousand
- receive only text fields of records, without getting a bunch of “heavy” pictures from the database
- there is still plenty options :)
At first, I also had no idea 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 will retrieve records only on demand, but accept filters in any order.
That's what came out of it.
iActiveRecord supports WHERE base conditions
The same can be described in the style of familiar and convenient NSPredicate'ov
I almost never used this myself, but decided that for completeness I need to implement it.
Different types of join
are supported - ARJoinLeft
- ARJoinRight
- ARJoinInner
- ARJoinOuter
I think the names speak for themselves.
There is one small crutch associated with this feature, so you need to call
instead
This method returns an array from dictionaries, where the keys are entity names and the values are data from the database.
The database can be stored both in Caches and in Documents, and in case of storage in Documents, the attribute that disconnects the backup is added to the file
otherwise, the application will receive reject from Apple.
The github project is iActiveRecord .
Opportunities
Tojustify the conclusion, I want to say that the project started just for fun, and it continues to develop, with plans to eventually clean up a bunch of "dirty" code and add other useful features.
I am pleased to hear adequate criticism.
PS write error messages to the LAN, please.
When I was just starting to use CoreData in iOS development, even then there were thoughts that this interaction could be somehow simplified. After some time, I met with ActiveRecord from RubyOnRails, and then I realized what I was missing.
Having searched a little on the github I found a lot of implementations, but for various reasons I did not like them. Some are written for CoreData, but I do not like it, in others you need to create tables with your hands, or write raw sql queries. And in some cases the code was terribly terrible, sometimes I’m not writing it very cleanly myself, but the huge fence from the enclosed if / switch / if / switch is too much.
In the end, I decided to write my 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.
Here is what came of it all.
And under the cat there is a small description of the possibilities and implementation (in fact, there is a lot of text and pieces of code, a summary at the very end of the article).
Create tables
The first problem was creating 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 thought for a long time about how to arrange it better, and after a while it dawned on me.
Objective-C allows you to get a list of all subclasses for any class, and in addition to get a list of all its properties. Thus, the description of the entity will be a simple description of the class, and all that remains for me to do is to collect this information and compose 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 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 greater flexibility, I 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: it must be able to save and load itself.
To do this, it must implement ARRepresentationProtocol
@protocol ARRepresentationProtocol
@required
+ (const char *)sqlType;
- (NSString *)toSql;
+ (id)fromSql:(NSString *)sqlData;
@end
I implemented these methods for the Foundation framework types using the 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 with the Transformable data type allows you to achieve the same thing, but I still have not figured out how to work with it.
CRUD for records
Create
The process of creating a new record is very simple and transparent.
User *user = [User newRecord];
user.name = @"Alex";
[user save];
Read
Retrieving All Records
NSArray *users = [User allRecords];
Often, all the records are not needed, so I added an implementation of filters, but more 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 changes in all properties, and when updating 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 (NSNumber) property, which is used for deletion.
Unnecessary fields
What about fields that we don’t need to save to the database? Just ignore them :)
To do this, add the following construct to the class implementation, this is a simple macro macro.
@implementation User
...
@synthesize ignoredProperty;
...
ignore_fields_do(
ignore_field(ignoredProperty)
)
...
@end
Validation
One of the demands that I set for myself in development is the support of validations.
At the moment, two types of validation are implemented: for availability and for uniqueness.
The syntax is simple, and also uses col macros. In addition, the class must implement ARValidatableProtocol, nothing is required from the user, this is done so as not to start the validation mechanism for classes that do not use it.
// User.h
@interface User : ActiveRecord
...
@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 himself can add.
To do this, you need to create a validator class that must implement ARValidatorProtocol and describe it in a validated class.
ARValidatorProtocol
@protocol ARValidatorProtocol
@optional
- (NSString *)errorMessage;
@required
- (BOOL)validateField:(NSString *)aField ofRecord:(id)aRecord;
@end
Custom validator
// PrefixValidator.h
@interface PrefixValidator : NSObject
@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 is returned, you can get a list of errors
[user errors];
after which an array of objects of class ARError 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 based on which you can create a localized message and display it to the application user.
Migrations
Migrations are implemented at a primitive level: it only responds to adding new fields to entities or to adding new entities.
To use migrations, you don’t need to register anything anywhere.
At the first launch of the application, all tables are created, and at subsequent launches, the table is checked for new fields or if there are any, then alter table queries are made.
In order not to instantiate a check for changes in the table structures, you must 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 - an ordinary macro that throws an exception of type ARException.
Tarnzaktsii can be used not only for rollback in case of failure, but also to increase the speed of query execution when adding records.
One of the projects had a terrible brake when trying to create over9000 records. The dump execution time was about 180 seconds after I wrapped it in a BEGIN transaction; ... COMMIT; time decreased to ~ 4-5 seconds. So I advise everyone who is not in the know.
Communications
When I became acquainted with the implementation of ActiveRecord in RoR, I was delighted with the simplicity of creating a connection between entities. By and large, this simplicity served as the first prerequisite for the creation of this framework. And now I consider the most important feature in my bike to be just the connections 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 belongs_to_dec belonsg_to_imp macros accept three parameters: the name of the class we are “contacting” with, the name of the getter, and the type of dependency.
There are two types of dependencies: ARDependencyNullify and ARDependencyDestroy, the first when deleting the model nullifies its relationships, and the second removes all related entities.
The field for this relationship should 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 with BelongsTo type communications.
The main thing to remember: before creating a link, both records must be saved, otherwise they do not have an id, and the links 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 association has the same drawbacks as HasMany.
Macros * _dec / * _ imp add helper methods to add associations.
set#ModelName:(ActiveRecord *)aRecord; // BelongsTo
add##ModelName:(ActiveRecord *)aRecord; // HasMany, HasManyThrough
remove##ModelName:(ActiveRecord *)aRecord; // HasMany, HasManyThrough
Query Filters
Very often, it is required to somehow filter the selection from the database:
- search for records matching some kind of template (UISearchBar)
- display in the table only 5 records out of a thousand
- receive only text fields of records, without getting a bunch of “heavy” pictures from the database
- there is still plenty options :)
At first, I also had no idea 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 will retrieve 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 WHERE base 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 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];
Join
I almost never used this myself, but decided that for completeness I need to implement it.
- (ARLazyFetcher *)join:(Class)aJoinRecord
useJoin:(ARJoinType)aJoinType
onField:(NSString *)aFirstField
andField:(NSString *)aSecondField;
Different types of join
are supported - ARJoinLeft
- ARJoinRight
- ARJoinInner
- ARJoinOuter
I think the names speak for themselves.
There is one small crutch associated with this feature, so you need to call
- (NSArray *)fetchJoinedRecords;
instead
- (NSArray *)fetchRecords;
This method returns an array from dictionaries, where the keys are entity names and the values are 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 database can be stored both in Caches and in Documents, and in case of storage in Documents, the attribute that disconnects 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 reject from Apple.
Summary
The github project is iActiveRecord .
Opportunities
- ARC support
- unicode support
- migration
- validations, with the support of custom validators
- transactions
- custom data type
- communications (BelongsTo, HasMany, HasManyThrough)
- sorting
- filters (where =,! =, IN, NOT IN and others)
- associations
- CocoaPods support
Conclusion
To
I am pleased to hear adequate criticism.
PS write error messages to the LAN, please.