DKDBManager is a simple, yet very useful, CRUD manager around Magical Record (a wonderful CoreData wrapper). The current library will implement a logic around it and helps the developer to manage his entities.
Through the implemented CRUD logic you will be able to focus on other things than the “classic” repetitive and boring data management.
The main concept is to use JSON dictionaries representing your entities. The logic to create, read or update your entities is done with just one single function. The delete logic has also been improved with a deprecated
state.
Extending the NSManagedObject subclasses is required.
The complete documentation is available on CocoaDocs.
The library is explained using Swift code. For an Obj-C example see the sample projects.
DKDBManager is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod "DKDBManager"
and run pod install
from the main directory.
To get started, first import the header file DKDBManager.h in your project's .pch or bridge-header file. This will import all required headers for Magical Record
and CoredData
.
#import "DKDBManager.h"
As the DKDBManager
is a light wrapper around Magical Record you’ll first need to implement some minor methods within your AppDelegate. After that you still have to generate your model classes and create categories (or extensions in Swift).
First you need to setup the CoreData stack with a specific file name. Of course you can play with the name to change your database on startup whenever you would like to.
A good practice will be to call this method at the beginning of the application:application didFinishLaunchingWithOptions:launchOptions
method of your AppDelegate
.
You could also subclass the DKDBManager and wrap the following in a dedicated class.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
var didResetDB = DKDBManager.setupDatabaseWithName("DKDatabaseName.sqlite")
if (didResetDB) {
// The database is fresh new.
// Depending on your needs you might want to do something special right now, such as:
// - Setting up some user defaults.
// - Deal with your api/store manager.
// etc.
}
// Starting at this point your database is ready to use.
// You can now create any object you want.
return true
}
func applicationWillTerminate(application: UIApplication) {
DKDBManager.cleanUp()
}
You can configure how the manager will react on execution. Add the following optional lines before calling setupDatabaseWithName:
:
+ verbose to toggle the log.
DKDBManager.setVerbose(true)
+ allowUpdate to allow the manager to update the entities when parsing new data.
DKDBManager.setAllowUpdate(true)
+ resetStoredEntities to completely reset the database on startup. Instead of removing your app from the simulator just activate this flag and the local DB will be brand new when your app starts.
DKDBManager.setResetStoredEntities(false)
+ needForcedUpdate to force the manager to update the entities during the CRUD process.
DKDBManager.setNeedForcedUpdate(false)
The model configuration is done as in any other CoreData project.
First create and configure your model entities inside the .xcdatamodel
file.
Then generate with Xcode the NSManagedObject subclasses as you are used to.
After that, create category files (or extensions in Swift) for each model class. The functions and logic will be implemented in those files. (If it were to be done in the generated files, your changes would be removed every time you generate them again.)
Example: Book.swift
and Book+DKDBManager.swift
warning If your code is in Swift you can either generate the NSManagedObject subclasses in Swift or Obj-C.
-
Swift: add
@objc(ClassName)
before the implementation:@objc(Book) class Book: NSManagedObject { @NSManaged var name: NSString? @NSManaged var order: NSNumber? }
-
Obj-C: import the class header in the bridge-header file.
#import "Book.h"
This part explains how to create and configure a single local database that doesn’t need to be updated from an API. In this case there is no need for deprecated entities or automatic updates.
As explained earlier, you first need the data inside a NSDictionary object. This data will get parsed and the library will apply a CRUD logic on it.
In each extended class, implement the following methods:
+ primaryPredicateWithDictionary: to create a predicate used in the CRUD process to find the right entity corresponding to the given dictionary. This function should create and return a NSPredicate
object that match only one database entity.
If you need more information on how to create a NSPredicate object, please read the official documentation.
override public class func primaryPredicateWithDictionary(dictionary: [NSObject:AnyObject]!) -> NSPredicate! {
// If returns nil then only ONE entity will ever be created and updated.
return nil
// - OR -
// If returns a `false predicate` then a new entity will always be created.
return NSPredicate(format: "FALSEPREDICATE")
// - OR -
// Otherwise the CRUD process use the entity found by the predicate.
let bookName = GET_STRING(dictionary, "name")
return NSPredicate(format: "name ==[c] %@", bookName)
}
- updateWithDictionary: to update the current entity with a given dictionary.
override public func updateWithDictionary(dictionary: [NSObject : AnyObject]!) {
// Update attributes
self.name = GET_STRING(dictionary, "name")
self.order = GET_NUMBER(dictionary, "order")
}
- description to improve how the receiving entity is described/logged.
override var description: String {
get {
return "{ name: \(self.name), order: \(self.order) }"
}
}
+ sortingAttributeName to specify a default order for the + all and and + count functions.
override public class func sortingAttributeName() -> String! {
return "order"
}
+ verbose to toggle the log for the receiving class.
If this value returns true
and if the DKDBManager.verbose == true
then the manager will automatically print the entities for this class during the CRUD process. The number of objects and their (overriden) description will also be logged. Another very handy feature, even the activity around this class model will be printed: Updating Book { name: LOTR, order: 1 }
override public class func verbose() -> Bool {
return true
}
To create new entities in the current context you need to have your data inside a NSDictionary object. And then use one of the following functions:
+ createEntityFromDictionary:completion:
If you have multiple entities to create inside an array feel free to use:
To update or save as not deprecated just call the same function with the same dictionary.
The most important values are the ones required by the function primaryPredicateWithDictionary: to actually let the manager finds the entities again. This function should create and return a NSPredicate
object that match only one database entity.
The values not used to create the primary predicate could be missing or changed, the valid entity will still be found correctly.
let bookInfo = ["name":"LOTR", "order":1]
Book.createEntityFromDictionary(entityInfo) { (newBook, state) -> Void in
//
// The CRUDed entity is referenced in the `newBook`.
// Its actual state is described as follow:
switch state {
case .Create: // The book has been created, it's all fresh new.
case .Update: // The book has been updated, its attributes changed.
case .Save: // The book has been saved, nothing happened.
case .Delete: // The book has been removed.
}
}
It is also possible to simply update an entity once it is instantiated in your code.
The changes will only be made in the current context. If you want the changes to persist, you need to save
.
class ViewController : UIViewController {
var aBook : Book?
func awesomeFunction() {
// Update the entity.
self.aBook?.name = "The Hobbit"
// Save the current context to the persistent store.
DKDBManager.save()
}
}
When you are doing modifications on the DB entites, you still need to save the current context into the persistent store (aka the sqlite file).
To do so use one of the following methods:
DKDBManager.saveToPersistentStoreAndWait()
DKDBManager.saveToPersistentStoreWithCompletion(void ( ^ ) ( BOOL success , NSError *error ))
Those calls need to be done after creating, deleting or updating entities. Saving to the persistent store is not an easy task, please try to conserve the CPU usage and call those functions only when necessary.
To read the entities in the current context you need to fetch them using a NSPredicate
and Magical Record
.
Using the various methods of Magical Record can seriously help you on your everyday development. Here is the official documentation.
Here is an example showing how to fetch an array of entities depending on their name:
class func entityForName(name: String) -> [Book] {
var predicate = NSPredicate(format: "name == \(name)")
var entities = Book.MR_findAllSortedBy(Book.sortingAttributeName(), ascending: true, withPredicate: predicate)
return (entities as? [Book] ?? [])
}
Note that the call to Book.sortingAttributeName() returns the default sorting attribute previously set. Instead, you could also provide a specific sorting attribute name (e.g.: "order").
To delete an entity use - deleteEntityWithReason:. It will delete the current entity, log the reason (if logging is enabled) and forward the delete process to the child entities using the function - deleteChildEntities (see How to deal with child entities).
aBook.deleteEntityWithReason("removed by user")
If you want to be more radical and remove all entities for the current class you can use + deleteAllEntities or + deleteAllEntitiesForClass:.
Book.deleteAllEntities()
// - OR -
DKDBManager.deleteAllEntitiesForClass(Book)
Attention, if you call + deleteAllEntities on the DKDBManager all entities for all classes will be deleted.
DKDBManager.deleteAllEntities()
The recommended logic about child entities is to directly add their information in their parent's data. Meaning, the NSDictionary object used to create a parent entity should also contains the information to create the child entities.
With such structure, the DKDBManager implements a cascade process to create, update, save and delete child entities.
Some other functions must be implemented.
The function - updateWithDictionary: should be used to forward the CRUD process to the child classes.
override public func updateWithDictionary(dictionary: [NSObject : AnyObject]!) {
// Update attributes
self.name = GET_STRING(dictionary, "name")
self.order = GET_NUMBER(dictionary, "order")
// CRUD child entities
let array = OBJECT(dictionary, "pages")
Page.createPagesFromArray(array, book: self)
}
The relation between a Book
and a Page
is a one-to-many
as a book has many pages and a page has just one book.
The custom function createPagesFromArray
inserts in the dictionary the parent book entity.
extension Page {
class func createPagesFromArray(array: AnyObject?, book: Book?) {
// CRUD pages
for dict in (array as? [[NSObject : AnyObject]] ?? [[:]]) {
var copy = dict
// Insert the parent book object
copy["book"] = book
Page.createEntityFromDictionary(copy) { (entity: AnyObject? , state: DKDBManagedObjectState) -> Void in
if (entity != nil && book != nil) {
switch state {
// Remove the page from the parent's 'pages' NSSet.
case .Delete: book?.removePagesObject(entity as? Page)
// Add the page into the parent's 'pages' NSSet.
case .Create: book?.addPagesObject(entity as? Page)
case .Save, .Update: break // Do nothing.
}
}
}
}
}
}
Warning: The methods removePagesObject
and addPagesObject
exist only if you generated your model classes in Obj-C.
If you did it in Swift you need to manually add small functions do add
and remove
child entities.
func addPage(page: Page) {
var bookSet = self.mutableSetValueForKey("pages")
bookSet.addObject(page)
self.pages = bookSet
}
The CRUD process will then call the following function to update the Page
entity. Use this one to set the parent book object.
override public func updateWithDictionary(dictionary: [NSObject : AnyObject]!) {
// Super update
super.updateWithDictionary(dictionary)
// Setup parent Book
self.book = OBJECT(dictionary, "book") as? Book
// Update attributes
self.text = GET_STRING(dictionary, "text")
}
When a parent entity got saved as not deprecated (manually or through the CRUD process) the - updateWithDictionary: function is not called and no CRUD logic reaches the child entities.
To complete the cascase process to save the child entities, each model class should implement the - saveEntityAsNotDeprecated method and forward it to its children.
public override func saveEntityAsNotDeprecated() {
// Method to save/store the current object AND all its child relations as not deprecated.
super.saveEntityAsNotDeprecated()
// Forward the process
for page in (self.pages as? Set<Page> ?? []) {
page.saveEntityAsNotDeprecated()
}
}
For more information about deprecated entities please read the Database matching an API section.
The CRUD process or a specific user action might needs to delete an entity. In most cases the child entities need to be removed as well.
This cascade removal allows the developer to remove a complete structure of entities in just one single line:
// Delete a book and all its pages and other child entities
aBook.deleteWithReason("user does not want it anymore")
To make sure the pages
of this deleted book
are also deleted implement the - deleteChildEntities function.
// remove all children of the current object
public override func deleteChildEntities() {
// super call
super.deleteChildEntities()
// Forward the delete process to every child.
for page in (self.pages as? Set<Page> ?? []) {
page.deleteEntityWithReason("parent book removed")
}
}
For many projects it is required to match a database hosted in a server and accessible through an API. In most cases it delivers a big JSON data containing all informations about the entities. Use the CRUD process to create and update your local models based on that data.
A good practice is to receive all this within a "cascade" structure.
Here is an example of data structure with books
containing pages
containing images
:
{
"books" : [
{
"name":"LOTR",
"order":1,
"pages": [
{
"text": "abcde",
"images": [
...
]
}
]
},
{
"name":"The Hobbit",
"order":2
"pages" : [ ... ]
}
]
}
But what about entities that do not exist anymore in the API? What about badly updated entities that become invalid? What about unchanged values and useless update processes?
The DKDBManager helps you deal with those cases just by implementing a few additional functions.
Sometimes you might need to update just some entities and not all of them. But how can you do so when the CRUD process iterates through the whole JSON structure entity by entity?
The DKDBManager lets you choose whether an object needs an update or not. The function - shouldUpdateEntityWithDictionary receives as a parameter the JSON structure as a NSDictionary object. You can use the current entity and the parameter to check if an update is needed or not.
For example; is the updated_at
field the same in the JSON and in the local database?
-
If so, return
false
and no update process will occur -
Otherwise return
true
and the - updateWithDictionary: function will be called.
Due to the cascade process if a parent entity should NOT be updated then its child entities shouldn't either.
In order to avoid useless actions the update verification will stop at the first valid and already updated parent object. Its children will not get updated and the DKDBManager will not even ask them if they should be.
Here is an implementation of the function - shouldUpdateEntityWithDictionary that checks the lastest update_at
:
override public func shouldUpdateEntityWithDictionary(dictionary: [NSObject:AnyObject]!) -> Bool {
// if the updated_at value from the dictionary is a different one.
let lastUpdatedAt = (GET_DATE(dictionary, "updated_at") ?? NSDate())
return (self.updated_at.isEqualToDate(lastUpdatedAt) == false)
}
TIP
The parent updated_at
timestamp should be refresh everytime one of its child gets updated. If so you will never miss a small update of the children or grand children of an entity.
A deprecated entity is a NSManaged object not saved as not deprecated
in the DKDBManager.
An object not saved as not deprecated is:
-
An object where the CRUD process didn't go through (isn't sent by the API anymore?).
-
An object marked as invalid (see Invalid model entities section).
-
An object where a manual - saveEntityAsNotDeprecated did not occur.
-
An object where the CRUD process has a state that equals to
.Delete
To remove the deprecated entities call the function + removeDeprecatedEntities after the CRUD process (when refreshing the local database from an API) and before saving
the current context to the persistent store.
// CRUD process
DKDBManager.createEntityFromDictionary(data)
// Remove all deprecated entities (not set as 'not deprecated')
DKDBManager.removeDeprecatedEntities()
// Save the current context to the persistent store
DKDBManager.saveToPersistentStoreWithCompletion() { /* Do something */}
If an entity did not change in the backend but is still sent through the API, your local database needs to save it as not deprecated. But, as explained previously, if nothing changed the cascase update process will stop on the first entity and not forward the process to its children. By doing so they won't get updated nor saved as not deprecated.
When the + removeDeprecatedEntities occurs those unsaved entities will be removed.
To avoid such problems, implement the function - saveEntityAsNotDeprecated inside your NSManagedObject subclasses. It will be called on the first valid parent model object and will be forwarded to every child.
For more information, see the How to deal with child entities section.
The DKDBManager allows you to define whether an entity is invalid or not.
During the CRUD process an entity is tested to verify its validity. If - invalidReason returns nil, no invalidReason
has been found. Otherwise the reason will automatically be used and logged by the function - deleteEntityWithReason:.
The function - invalidReason must be implemented inside a NSManagedObject subclass.
Here is an example of implementation:
public override func invalidReason() -> String! {
// Check the super class first.
if let invalidReason = super.invalidReason() {
return invalidReason
}
// Then verify the current required attributes.
if (self.image == nil) {
return "missing full screen image URL"
}
if (self.pages.count == 0) {
return "invalid relation with: Page - no entities found"
}
return nil
}
If this function has been implemented, you can also manually delete invalid entities: - deleteIfInvalid
aBook.deleteIfInvalid()
Depending on the app and its architecture some entities could get invalid when something important has been removed or updated.
Or some could also become invalid after removing the deprecated entities
.
If this is the case you have to check the deprecated entities manually.
To do so use the function - deleteIfInvalid on your model objects.
class func checkAllDeprecatedEntities() {
//
// Check validity of ALL books and remove the invalid ones.
if let books = self.all() as? [Book] {
for book in books {
book.deleteIfInvalid()
}
}
}
-
Add more custom functions inside the helper files. All logic related to a class model should/could be inside this file. It helps a lot to structure the code.
-
Subclass the DKDBManager and use this new class to add more DB related functions. It keeps the
AppDelegate
cleaner. -
Generate the model classes in Obj-C. You will have no trouble with
optional
variables. Plus, theNSSet
objects forto-many
-relationships in the DB will have additional functions.
DKDBManager
is used in the following projects:
- WhiteWall
- Pons-SprachKalender
- Pons-Bildwoerterbuch
- ERGO ZahnPlan
- Digster Music Deals
- Handhelp
- RezeptBOX
- Huethig
- Your project here
kevindelord, [email protected]
DKDBManager is available under the MIT license. See the LICENSE file for more info.