{"id":958,"url":"https://github.com/justeat/JustPersist","last_synced_at":"2025-08-06T13:32:01.432Z","repository":{"id":56916990,"uuid":"83094609","full_name":"justeat/JustPersist","owner":"justeat","description":"JustPersist is the easiest and safest way to do persistence on iOS with Core Data support out of the box. It also allows you to migrate to any other persistence framework with minimal effort.","archived":true,"fork":false,"pushed_at":"2024-01-05T10:33:07.000Z","size":524,"stargazers_count":164,"open_issues_count":0,"forks_count":10,"subscribers_count":21,"default_branch":"master","last_synced_at":"2024-12-01T07:52:14.165Z","etag":null,"topics":["coredata","database","ios","model","persistence"],"latest_commit_sha":null,"homepage":"https://tech.justeattakeaway.com/","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/justeat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2017-02-25T00:33:19.000Z","updated_at":"2024-09-03T13:55:49.000Z","dependencies_parsed_at":"2024-01-29T16:57:57.779Z","dependency_job_id":"5c910a36-6612-44b4-9b20-ab449e6d3df5","html_url":"https://github.com/justeat/JustPersist","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justeat%2FJustPersist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justeat%2FJustPersist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justeat%2FJustPersist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justeat%2FJustPersist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/justeat","download_url":"https://codeload.github.com/justeat/JustPersist/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228905441,"owners_count":17989766,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["coredata","database","ios","model","persistence"],"created_at":"2024-01-05T20:15:35.578Z","updated_at":"2024-12-09T14:30:43.106Z","avatar_url":"https://github.com/justeat.png","language":"Swift","funding_links":[],"categories":["Core Data","Libs","Uncategorized","Data Management [🔝](#readme)"],"sub_categories":["Linter","Data Management","Uncategorized","Other free courses"],"readme":"![JustPersist Banner](./img/just_persist_banner.png)\n\n# JustPersist\n\n### Warning: This library is not supported by Just Eat Takeaway anymore and therefore considered deprecated. The repository has been archived.\n\n[![Build Status](https://travis-ci.org/justeat/JustPersist.svg?branch=master)](https://travis-ci.org/justeat/JustPersist)\n[![Version](https://img.shields.io/cocoapods/v/JustPersist.svg?style=flat)](http://cocoapods.org/pods/JustPersist)\n[![License](https://img.shields.io/cocoapods/l/JustPersist.svg?style=flat)](http://cocoapods.org/pods/JustPersist)\n[![Platform](https://img.shields.io/cocoapods/p/JustPersist.svg?style=flat)](http://cocoapods.org/pods/JustPersist)\n\nJustPersist is the easiest and safest way to do persistence on iOS with Core Data support out of the box. It also allows you to migrate to any other persistence framework with minimal effort.\n\n- [Just Eat Tech blog](https://tech.just-eat.com/2017/03/02/how-to-abstract-your-persistence-layer-and-migrate-to-another-one-on-ios-with-justpersist/)\n\n# Overview\n\nAt Just Eat, we persist a variety of data in the iOS app. In 2014 we decided to use [MagicalRecord](https://github.com/magicalpanda/MagicalRecord) as a wrapper on top of Core Data but over time the numerous [problems](https://github.com/magicalpanda/MagicalRecord/issues) and fundamental thread-safety issues, arose. In 2017, MagicalRecord is not supported anymore and new solutions look more appealing. We decided to adopt [Skopelos](http://github.com/albertodebortoli/Skopelos): a much younger and lightweight Core Data stack, with a simpler design, developed by [Alberto De Bortoli](http://twitter.com/albertodebo), one of our engineers. The design of the persistence layer interface gets inspiration from Skopelos as well, and we invite the reader to take a look at [its documentation](https://github.com/albertodebortoli/Skopelos/blob/master/README.md).\n\nThe main problem in adopting a new persistence solution is migrating to it. It is rarely easy, especially if the legacy codebase doesn't hide the adopted framework (in our case MagicalRecord) but rather spread it around in view controllers, managers, helper classes, categories and sometimes views. Ultimately, in the case of Core Data, there is a single persistent store and this is enough to make impossible to move access across \"one at a time\". There can only be one active persistence solution at a time.\n\nWe believe this is a very common problem, especially in the mobile world. We created JustPersist for this precise reason and to ease the migration process.\n\nAt the end of the day, JustPersist is two things:\n\n- A persistence layer with a clear and simple interface to do transactional readings and writings (Skopelos-style)\n- A solution to migrate from one persistence layer to another with (we believe) the minimum possible effort\n\nJustPersist aims to be the easiest and safest way for persistence on iOS. It supports Core Data out of the box and can be extended to transparently support other frameworks. Since moving from MagicalRecord to Skopelos, we provide available wrappers for these two frameworks. \n\nThe tone of JustPersist is very much Core Data-oriented but it enables you to migrate to any other persistence framework if a custom data store (wrapper) is implemented (in-memory, key-value store, even [Realm](https://realm.io/) if you are brave enough).\n\nJustPersist is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile:\n\n```ruby\npod \"JustPersist/Skopelos\"\n# or\npod \"JustPersist/MagicalRecord\"\n```\n\nUsing only `pod JustPersist` will add the core pod with no subspecs and you'll have to implement your own wrapper to use the it. If you intend to extend JustPersist to support other frameworks, we suggest creating a subspec.\n\n\n# Usage of the persistence layer\n\nTo perform operation you need a data store, which you can setup like this (or see [related paragraph](#common-way-of-setting-up-a-data-store) paragraph):\n\n```swift\nlet dataStore = SkopelosDataStore(sqliteStack: \u003cmodelURL\u003e)\n// or\nlet dataStore = MagicalRecordDataStore()\n```\n\nBefore using the data store for the first time, you must call `setup()` on it, and possibly `tearDown()` when you are completely done with it.\n\nWe suggest setting up the stack at app startup time, in the `applicationDidFinishLaunchingWithOptions` method in the AppDelegate and to tear it down at the end of the life cycle of your entire app, when resetting the state of the app (if you provide support to do so) or in the `tearDown` method of your unit tests suite.\n\nTo hide the underlying persistence framework used, JustPersist provides things that conform to `DataStoreItem` and `MutableDataStoreItem`, rather than the CoreData specific `NSManagedObject`. These protocols provide access to properties using `objectForKey` and `setObject:forKey:` methods.\n\nIn the case of Core Data, JustPersist provides an extension to `NSManagedObject` to make it conforming to `MutableDataStoreItem`.\n\n\n## Readings and writings\n\nThe separation between readings and writings is the foundation of JustPersist.\nReading are always synchronous by design:\n\n```swift\ndataStore.read { (accessor) in\n  ...\n}\n```\n\nWhile writings can be both synchronous or asynchronous:\n\n```swift\ndataStore.writeSync { (accessor) in\n  ...\n}\n\ndataStore.writeAsync { (accessor) in\n  ...\n}\n```\n\nThe accessor provided by the blocks can be a read one (`DataStoreReadAccessor`) or a read/write one (`DataStoreReadWriteAccessor`). Read accessors allow you to do read operations such as:\n\n```swift\nfunc items(forRequest request: DataStoreRequest) -\u003e [DataStoreItem]\nfunc firstItem(forRequest request: DataStoreRequest) -\u003e DataStoreItem?\nfunc countItems(forRequest request: DataStoreRequest) -\u003e Int\n```\n\nWhile the read/write ones allow you to perform a complete set of CRUD operations:\n\n```swift\nfunc mutableItems(forRequest request: DataStoreRequest) -\u003e [MutableDataStoreItem]\nfunc firstMutableItem(forRequest request: DataStoreRequest) -\u003e MutableDataStoreItem?\nfunc createItem(ofMutableType itemType: MutableDataStoreItem.Type) -\u003e MutableDataStoreItem?\nfunc insert(_ item: MutableDataStoreItem) -\u003e Bool\nfunc delete(item: MutableDataStoreItem) -\u003e Bool\nfunc deleteAllItems(ofMutableType itemType: MutableDataStoreItem.Type) -\u003e Bool\nfunc mutableVersion(ofItem item: DataStoreItem) -\u003e MutableDataStoreItem?\n```\n\nTo perform an operation you might need a `DataStoreRequest` which can be customized with itemType, an NSPredicate, an array of NSSortDescriptor, offset and limit. Think of it as the corresponding Core Data's `NSFetchRequest`.\n\n\nHere are some complete examples:\n\n```swift\ndataStore.read { (accessor) in\n  let request = DataStoreRequest(itemType: Restaurant.self)\n  let count = accessor.countItems(forRequest: request)\n}\n\ndataStore.read { (accessor) in\n  let request = DataStoreRequest(itemType: Restaurant.self)\n  request.setFilter(whereAttribute: \"name\", equalsValue: \u003csome_name\u003e)\n  guard let restaurant = accessor.firstItem(forRequest: request) as? Restaurant else { return }\n  ...\n}\n\ndataStore.writeSync { (accessor) in\n  let restaurant = accessor.createItem(ofMutableType: Restaurant.self) as! Restaurant\n  restaurant.name = \u003csome_name\u003e\n  ...\n  let wasDeleted = accessor.delete(item: restaurant)\n}\n```\n\nIn write blocks there is no need to make any call to a save method. Since it would be the obvious thing to do at the end of a transactional block, JustPersist does it for you. Read blocks are not meant to modify the store and you wouldn't even have the API available to do so (unless `DataStoreItem` objects are casted to `NSManagedObject` in the case of CoreData to allow the setting of properties), therefore a save will not be performed under the hood. \n\n\n## Common way of setting up a data store\n\nWe recommend to use dependency injection to pass the data store around but sometimes it might be hard. If you wish to access your data store via a singleton, here is how your app could create a shared instance for the DataStoreClient (e.g. `DataStoreClient.swift`) using Skopelos.\n\n```swift\n@objc\nclass DataStoreClient: NSObject {\n\nstatic let shared: DataStore = {\n  return DataStoreClient.sqliteStack()\n}()\n\nstatic let inMemoryShared: DataStore = {\n  return DataStoreClient.inMemoryStack()\n}()\n\nclass func sqliteStack() -\u003e DataStore {\n  let modelURL = Bundle.main.url(forResource: \"\u003cschema_filename\u003e\", withExtension: \"momd\")! // want to crash if schema is missing\n  return SkopelosDataStore(sqliteStack: modelURL, securityApplicationGroupIdentifier: \u003csecurity_application_group_identifier_id_any\u003e) { error in\n    print(\"Core Data error reported via SkopelosDataStore (sqliteStack): \\(error.localizedDescription)\")\n  }\n}\n\nclass func inMemoryStack() -\u003e DataStore {\n  let modelURL = Bundle.main.url(forResource: \"\u003cschema_filename\u003e\", withExtension: \"momd\")! // want to crash if schema is missing\n  return SkopelosDataStore(inMemoryStack: modelURL) { error in\n    print(\"Core Data error reported via SkopelosDataStore (inMemoryStack): \\(error.localizedDescription)\"\")\n  }\n}\n```\n\nFor unit tests, you might want to use the `inMemoryShared` for better performance.\n\n\n## Child data store\n\nA child data store is useful in situations where you might have the need to rollback all the changes performed in a specific section of the app or in a part of the user journey. Think of it as a scratch/disposable context in the [Core Data stack](http://martiancraft.com/blog/2015/03/core-data-stack/) by Marcus Zarra.\n\nAt Just Eat we use a child data store for the addition of complex products to the basket. The user might make many updates to the product and it is easier to perform the final save operation when the user confirms the addition rather than dealing with multiple CRUD operations on the main data store.\n\nA child data store behaves just like a normal data store, with the only exception that, to save the changes back to the main data store, developers must explicitly merge the data stores. Here is a complete example: \n\n```swift\nlet childDataStore = dataStore.makeChildDataStore()\nchildDataStore.setup()\n...\ndataStore.merge(childDataStore)\nchildDataStore.tearDown()\n```\n\n## Thread-safety notes\n\nRead and sync write blocks are always performed on the main thread, no matter which thread calls them.\nAsync write blocks are always performed on a background thread.\n\nSync writings return only when the changes are persisted (in the case of Core Data, usually to the `NSManagedObjectContext` with main concurrency type). \n\nAsync writings return immediately and leave the job of saving to the source of truth to JustPersist (whether it be the context or a persistent store). They are eventual consistent, meaning that the next reading could potentially not have the data available.\n\nForcing a transactional programming model for readings and writings helps developers to avoid thread-safety issues which in Core Data can be caught setting the `-com.apple.CoreData.ConcurrencyDebug 1` flag in your scheme (which we recommend enabling).\n\n\n# How to migrate to a different persistence layer\n\nExamples in this sections are in Objective-C as 1. they deal with the legacy code for the nature of the example and 2. to show that JustPersist works just fine with Objective-C too.\n\nHere we'll outline the steps we made to migrate away from MagicalRecord to Skopelos using JustPersist. We believe that a lot of apps still use MagicalRecord, so this may apply to your case too. If your need is to move from and to other 2 frameworks, you need to implement the corresponding data stores to wrap them.\n\nYou should start by implementing your `DataStoreClient` (you could follow the steps in the [related paragraph](#common-way-of-setting-up-a-data-store)) and allocating the data store for the current persistence layer used by your app in the `sqliteStack` method and possibly in the `inMemoryStack` one too. In our case, since we want to move away from MagicalRecord, the data store used would be `MagicalRecordDataStore`.\n\nStandard CRUD interactions with MagicalRecord are like so:\n\n```objective-c\nNSManagedObjectContext *mainContext = [NSManagedObjectContext MR_defaultContext];\nNSManagedObjectContext *childContext = [NSManagedObjectContext MR_contextWithParent:mainContext];\n    \n// writing (Create)\n[childContext performBlockAndWait:^{\n    Restaurant *restaurant = [Restaurant MR_createEntityInContext:localContext];\n    [childContext MR_saveToPersistentStoreAndWait];\n}];\n\n// reading (Read)\n[childContext performBlockAndWait:^{\n    Restaurant *restaurant = [Restaurant MR_findFirstInContext:childContext];\n}];\n\n// writing (Update)\n[childContext performBlockAndWait:^{\n    Restaurant *restaurant = [Restaurant MR_findFirstInContext:childContext];\n    restaurant.name = \u003csome_name\u003e\n    [childContext MR_saveToPersistentStoreAndWait];\n}];\n\n// writing (Delete)\n[childContext performBlockAndWait:^{\n    [Restaurant MR_truncateAllInContext:localContext];\n    [childContext MR_saveToPersistentStoreAndWait];\n}];\n```\n\nAll of them should be converted one by one to JustPersist:\n\n```objective-c\n\nDataStore *dataStore = [DataStoreClient shared];\n\n// writing (Create)\n[dataStore writeSync:^(id\u003cJEDataStoreReadWriteAccessor\u003e accessor) {\n    Restaurant *restaurant = (Restaurant *)[accessor createItemOfMutableType:Restaurant.class];\n}];\n\n// reading (Read)\n[dataStore read:^(id\u003cJEDataStoreReadAccessor\u003e accessor) {\n    JEDataStoreRequest *request = [[JEDataStoreRequest alloc] initWithItemType:Restaurant.class];\n    Restaurant *restaurant = (Restaurant *)[accessor firstItemForRequest:request];\n}];\n\n// writing (Update)\n[dataStore writeSync:^(id\u003cJEDataStoreReadWriteAccessor\u003e accessor) {\n    JEDataStoreRequest *request = [[JEDataStoreRequest alloc] initWithItemType:Restaurant.class];\n    Restaurant *restaurant = (Restaurant *)[accessor firstItemForRequest:request];\n    restaurant.name = \u003csome_name\u003e\n}];\n\n// writing (Delete)\n[dataStore writeSync:^(id\u003cJEDataStoreReadWriteAccessor\u003e accessor) {\n    [accessor deleteAllItemsOfMutableType:Restaurant.class];\n}];\n```\n\nYou should make sure you don't perform any UI work within the blocks even if the `read` and `writeSync` ones are executed on the main thread. Actually, you should aim for doing only the necessary work related to interact with the persistence layer, which often might be copying values out of objects to have them accessible outside the block (in Objective-C via the `__block` keyword). Developers should not hold references to model objects to pass them around threads (transactional blocks help ensure such rule).\n\nBy having moved all the direct interactions from MagicalRecord to JustPersist, you should be now able to remove all the various `@import MagicalRecord` and `#import \u003cMagicalRecord/MagicalRecord.h\u003e` from the entire codebase.\n\nOnce At this point, your `DataStoreClient` can be modified to allocate the target data store in the `sqliteStack` and `inMemoryStack` methods. In our case, the `SkopelosDataStore`.\n\n\n# Conclusion\n\nJustPersist aims to be the easiest and safest way to do persistence on iOS. It supports Core Data out of the box and can be extended to transparently support other frameworks.\n\nYou can use JustPersist to migrate from one persistence layer to another with minimal effort. Since we moved from MagicalRecord to Skopelos, we provide available wrappers for these two frameworks.\n\nAt its core, JustPersist is a persistence layer with a clear and simple interface to do transactional readings and writings, taking inspirations from Skopelos where readings and writings are separated by design.\n\nWe hope this library will ease the process of setting up a persistence stack, avoiding the common headache of Core Data and potential threading pitfalls.\n\n\n- Just Eat iOS team\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjusteat%2FJustPersist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjusteat%2FJustPersist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjusteat%2FJustPersist/lists"}