The main idea of ​​the repository is to abstract from data sources, hide implementation details, because, from the point of view of data access, this very implementation does not matter. Providing access to the main operations on data: save, load or delete is the main task of the repository.

Its advantages include:

  • no dependencies on the repository implementation. Anything can be under the hood: an in-memory collection, UserDefaults, KeyChain, Core Data, Realm, URLCache, a separate tmp file, etc.;
  • division of areas of responsibility. The repository acts as a layer between the business logic and the way data is stored, separating one from the other;
  • formation of a unified, more structured approach to working with data.

Ultimately, all this favorably affects the speed of development, scalability and testability of projects.

To the details

Consider the worst-case scenario for using Core Data.

For a general understanding, we will use the DataProvider class (hereinafter referred to as DP) – its task is to receive data from somewhere (network, UI) and put it in the Repository. Also, if necessary, DP can get data from the repository, acting as a facade for it. By data we mean an array of domain objects. It is with them that the repository operates.

1.Core Data as one big repository with NSManagamentObject

The idea of ​​operating NSManagedObject as a domain object is the simplest, but not the most successful one. With this approach, we face several problems at once:

  1. Core Data grows throughout the project, hanging unnecessary dependencies and violating areas of responsibility. Details of the implementation are disclosed. Know what the implementation of the repository is tied to – only the repository should;
  2. Using a single repository for all Data Providers, it will grow with the appearance of new domain objects;
  3. In the worst case, the logic of working with objects will start to overlap and this can turn into one big unpredictable magic.

2. Core Data + DB Client

The first thing that comes to mind to solve the problems from the previous example is to move the logic of working with objects into a separate class (let’s call it DB Client), then our Repository will only save and retrieve objects from the repository, while all the logic on work with objects will lie in the DB Client. The output should look something like this:

Both schemes solve problem # 1. (Core Data is limited to DB Client and Repository), and can partially solve problem # 2 and # 3 on small projects, but do not completely exclude them. Continuing the thought further, it is possible to come to the following scheme:

  1. Core Data can only be limited to a repository. DB Client converts domain objects to NSManagedObject and vice versa;
  2. The Repository is no longer one and does not grow;
  3. Data processing logic is more structured and consolidated

Here, it is worth noting that there are many options for composition and decomposition of classes, as well as ways of organizing interaction between them. Nevertheless, the above-described scheme shows another important problem: for each new domain object, at best, it is required to create X2 objects (DB Client and Repository). Therefore, we will consider another way of implementation.

Preparation for implementation

This is how the repository looks like:

  1. Domain object operated by the repository;
  2. The ability to subscribe to tracking changes in the repository;
  3. Saving objects to the repository;
  4. Saving objects with the ability to clean up old data within the same context;
  5. Loading data from the repository;
  6. Removing objects from the repository;
  7. Removing all data from the repository.

Perhaps your set of requirements for the repository will be different, but conceptually this will not change the situation.

Unfortunately, the ability to work with the repository through the AccessableRepository is missing, as evidenced by this error:

In this case, the Generic implementation of the repository is well suited, which looks like this:

  1. NSObject is needed to interact with NSFetchResultController;
  2. AccessableRepository – for clarity, transparency, and order;
  3. FatalError acts as a safeguard so that anyone who enters here does not use what is not implemented;

This solution allows you not to be tied to a specific implementation, and also to get around the previous problem:

To work with a selection of objects, you need an object with two properties:

  1. Selection condition, if the condition is absent, the selection is applied to the entire data set;
  2. Sorting condition – can be either absent or present.

Since, in the future, it may be necessary to use a separate NSPersistentContainer (for example, for testing), which, in turn, acts as a source of the context – you need to close it with a protocol:

  1. The context with which the main Queue operates is required to use the NSFetchedResultsController;
  2. Required to perform data operations on a background thread. Can be replaced with newBackgroundContext(). You can read about the differences in the work of these two methods here.

Also, you will need objects that will convert (mapping) domain models to repository objects (NSManagedObject) and vice versa:

  1. Allows you to convert NSManagedObject to a domain model;
  2. Allows updating NSManagedObject using the domain model;
  3. Used for communication between NSManagedObject and domain object.

You can use a domain object as an NSManagedObject initializer. On the one hand, it is convenient, but on the other hand, it imposes a number of restrictions. For example, when links between objects are used and one NSManagedObject creates several other NSManagedObject. This approach blurs areas of responsibility and negatively affects the overall logic of working with data.

While working with the repository, you will need to handle errors, an enum is enough for this:

Here the abstract part of the repository comes to an end, the most interesting thing remains – the implementation.

Implementation

For this example, a simple DBContextProvider implementation (without any additional parameters) will do:

  1. Initializing NSPersistentContainer;
  2. Previously, this approach eliminated memory leaks;
  3. DBContextProviding implementation.

The main part of the repository looks like this:

  1. The property that will be used when working with NSFetchRequest;
  2. DBContextProviding – to access the context, it is required to perform save, load, delete operations;
  3. fetchedResultsController – required when you want to track changes in the NSPersistentStore (changes to objects in the database);
  4. searchedData – Depends on fetchedResultsController and wraps the fetchedResultsController, hiding implementation details and notifying subscribers of data changes.
  5. entityMapper – converts domain objects to NSManagedObject and vice versa;
  6. Initializer;
  7. If autoUpdateSearchReques != nil, execute fetchedResultsController configuration to track changes in the database;

In order not to generate the same type of code for working with the context, you need a helper method:

  1. mergePolicy – Responsible for how conflicts are resolved when working with the context. In this case, the default policy is to give priority to modified objects in memory over persistent store;
  2. Saving changes to the persistent store;
  3. If there are no object changes, a corresponding error is passed to the completion block.

Saving objects is implemented as follows:

  1. It is used when it is necessary to delete objects, before saving new ones (within the current context);
  2. Objects that exist in the repository are unloaded for further modification;
  3. If there is no entity with the desired entityAccessorKey, a new NSManagedObject instance is created;
  4. Mapping properties from the domain object to the NSManagedObject;
  5. Applying the changes made.

Important: This solution is optimal for small datasets. For large data sets, it is recommended to split them into parts, use batchUpdate and batchDelete, and starting with IOS 13 batchInsert appeared.

Thus, the implementation of the save methods is reduced to calling the saveIn method:

The present, delete, eraseAllData methods are tied to working with NSFetchRequest. There is nothing special about their implementation:

  1. Creation of a request;
  2. Selection of objects and their processing;
  3. Return of the result of the operation.

To implement the ability to track data changes in real-time, you need a FetchedResultsController. The following method is used to configure it:

  1. Formation of a request on the basis of which changes will be tracked;
  2. Creating an instance of the NSFetchedResultsController class;
  3. performFetch () allows you to execute a request and get data without waiting for changes in the database. For example, this can be useful when implementing Ofline First;
  4. Changing the searchedData property, in turn, notifies subscribers (if any) of the change.

Conclusion

At this stage, the implementation of all the basic methods for working with the repository comes to an end. The main advantages are:

  • the logic of work of the repository with Core Data has become the same everywhere;
  • to add new objects to the repository, it is enough to create only the EntityMapper (a new Entity must be created in any case). All property mapping logic is also collected in one place;
  • The data layer has become more structured. You can now make sure that the repository does not make a huge number of requests in the save method to establish relationships between objects;
  • the repository can be easily changed, for example, for tests, or for debugging.

This approach may not suit everyone, and this is its main disadvantage. Often, the logic of working with data depends on the back-end as well.