Creating a Reusable and Generic Database Layer in Swift 5

Learn to use Mapper, the magical class

Waseem Khan
Better Programming
Published in
6 min readOct 19, 2019

--

Photo by fabio on Unsplash

This piece is focused on creating a reusable and generic database (DB) layer and explaining how to execute a specific implementation, in this case, Core Data.

Preliminary Notes

A few golden rules/suggestions:

  1. The DB layer should never be exposed to the view layer (view controllers).
  2. Only the service should be able to call the DB layer to do create, read, update, and delete (CRUD) operations.
  3. The internal working of the DB layer should never be exposed to the outer layers.
  4. The DB layer must be written in such a way that the DB client can be changed later, if required, without making too many changes, for example, if you use Core Data initially and then want to use Realm instead.
  5. DB entities shouldn’t be exposed to outer layers. View controllers or services should not know about entities/model classes, such as NSManagedObject subclasses. This will reduce the coupling of layers, and DB implementation can be switched easily later.

What this piece will cover:

  1. Preparing a generic DB layer,
  2. Creating a specific implementation of a DB layer,
  3. Providing a mapper layer for the DB implementation in Step Two.

Step One: Prepare a Generic DB Layer

We’ll begin by creating a generic DB layer.

Any entity that needs to be saved to the database should implement the Storable protocol.

StorageContext consists of generic DB operations that are required with almost any DB implementation

The naming convention for this tutorial:

  1. DBEntity: The entity that implements the Storable interface. This entity can be persisted to the database. In our case, NSManagedObject subclasses are DBEntities. For example, StoryEntity is a DBEntity.
  2. DomainEntity: The entity that implements the Mappable interface. This entity can be used on the view and business layers. This cannot be saved to the database. An example of a DomainEntity is Story.

Sorted class is required to give a factor to sort the results on.

The most common databases come with both a concrete and an in-memory implementation. Core Data has an in-memory type that can be used for unit testing. Enum ConfigurationType supports this need.

Step Two: Create a Specific Implementation of the DB Layer

I’ll use Core Data here to offer an implementation for the DB layer that I created in Step One. Core Data has some functions that are required in most cases, so let's add these methods as an extension of the StorageContext. CoreData entities are identified by NSManagedObjectID; we’ll need this method when fetching existing objects by ID from the database. You can also add other similar methods as needed.

CoreDataStorageContext is the implementation of the StorageContext. This is the specific implementation I talked about earlier.

And yes, CoreDataStorageContext implements all the required methods from theStorageContext protocol. To be noted here is that all the methods are expecting entities of theStorable type.

CoreDataStoreCoordinator is the class responsible for the initialization of the database and setting up all the prerequisites. I’ve included this class in the piece so you can understand the end-to-end implementation of the DB layer.

All Core Data entities inherit the NSManagedObject, and by default, NSManagedObject does not implement the Storable protocol. To mark the NSManagedObject as storable we need to conform it to theStorable.

This is what a sample StoryEntity looks like. Remember that StoryEntity is a DBEntity, i.e. it can be persisted to the DB.

Step Three: Prepare a Mapping Layer for DB Implementation

As I mentioned at the start of the piece, domain entities and database entities should be different. View controllers and the business (service) layer should not know about the database entities; the view controller should not know what a NSManagedObject is. So this step is specifically to serve that purpose. If you prefer not to create different versions of your model classes, you may not need this step.

We have seen before that all our DBEntities should implement the Storable protocol. Similarly, all our domain entities should implement the Mappable protocol.

You may have noticed that I declared a property calledNSManagedObjectID in the Mappable. I did this because we are working with Core Data and CoreData entities defined by NSManagedObjectID. This is required while mapping domain entities to DB entities. You may not need it if you already have a custom ID for your entities, such as story number.

Instead of adding the property NSManagedObjectID in each of our domain entities, I have added it to the Mappable protocol.

I created a base entity for all of my domain entities. All domain entities should inherit from this DomainBaseEntity. The NSManagedObjectID property from the Mappable protocol is defined in DomainBaseEntity. So none of the model classes needs to provide this property.

This is how the sample domain entity will look. The reason to implement the Codable interface is for automatic mapping from the JSON to the object. I have explained this in detail in my piece here

Now we are all set to create a mapping layer for the storage context to our business layer, and vice versa.

BaseDao is the parent of all the data access object (DAO) classes. It has methods that can be performed on the StorageContext. It declares the StorageContext as a dependency. You can pass any implementation of StorageContext here, for example,CoreDataStorageContext or RealmStorageContext. You will see that BaseDao expects two types of entities: Domain and DB. DomainEntity should be of type Mappable while DBEntity should conform to protocol Storable. These entities are required for mapping between domain and DB entities.

Here we have the magical class, Mapper, which maps the entities from domain to DB and vice versa. I used the Runtime library for iterating over the properties and copying them from the domain entity to a DB entity and vice versa.

This is a specific StoryDao which subclasses the BaseDao. Every subclass of BaseDao should provide the Domain and DB entity. In the case of StoryDao the Domain entity is Story and the DBentity is StoryEntity. I prefer to create a different DAO for every entity/database table.

For ease of use, I have created a DBManager to initialize the required DAOs. We need to provide the StorageContext implementation while initializing the DAO classes. StorageContext is the dependency for DBManager and should be set before calling any DAO. That way, you can change the StorageContext implementation at runtime. We can also provide a StorageContext with different configuration types, such as in-memory while running the test cases.

The ideal place to provide the StorageContext implementation is at the start of the app. However, it can be changed depending on your needs.

Testing

Talk is cheap, show me the code. — Linus Torvalds

Now we’re all set. We’ve successfully created a database layer that we can start using directly from the service. In the below snippet I’ve passed a new story object to the createStory() method and saved this to the Story table in the database. All we had to do was provide the story object. Our storyDAO already knows the DB entity that should be used to save the story objects.

There’s Always Room for Improvement

  1. Domain entities are not 100% independent since they have to inherit from DomainBaseEntity. DomainBaseEntity can be omitted if you have some custom identifier in each entity, such as storyNumber.
  2. Basic DB CRUD operations are being repeated in StorageContext and BaseDao. This is a tradeoff due to having different entities for domain and database. If you have a use case where you want to use the same domain and database entities in the whole project, you don’t need BaseDao. In this scenario, your DAOs, such as StoreDao, can directly inherit from StorageContext and can even provide custom methods if required.

I hope everything is clear. You can download the complete project from Github.

Leave a comment in case of any confusion. I would be happy to help. Suggestions are welcome.

--

--

Responses (2)