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).

    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 justify 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.

    Also popular now: