Accepting feedback on the new CoreStoreObject system #147

Closed
opened 2025-12-29 15:25:31 +01:00 by adam · 51 comments
Owner

Originally created by @JohnEstropia on GitHub (Jun 10, 2017).

If anyone is already using the new CoreStoreObject, I would gladly hear your your feedback, observations, and suggestions 🙂

Right now the CoreStoreObject more or less is as functional as NSManagedObject in common scenarios, but I might be missing some use-cases so I would like to hear everyone's opinion.

Originally created by @JohnEstropia on GitHub (Jun 10, 2017). If anyone is already using the new [`CoreStoreObject`](https://github.com/JohnEstropia/CoreStore#type-safe-corestoreobjects), I would gladly hear your your feedback, observations, and suggestions 🙂 Right now the `CoreStoreObject` more or less is as functional as `NSManagedObject` in common scenarios, but I might be missing some use-cases so I would like to hear everyone's opinion.
adam added the help wanted label 2025-12-29 15:25:31 +01:00
adam closed this issue 2025-12-29 15:25:31 +01:00
Author
Owner

@athei commented on GitHub (Jun 11, 2017):

Is it possible to use lightweight migration when renaming an attribute or relation? When using a model file you could define a renaming identifier. Is this possible with CoreStoreObject? When using CustomSchemaMappingProvider to define the mapping its always heavyweight, right?

@athei commented on GitHub (Jun 11, 2017): Is it possible to use lightweight migration when renaming an attribute or relation? When using a model file you could define a renaming identifier. Is this possible with `CoreStoreObject`? When using `CustomSchemaMappingProvider` to define the mapping its always heavyweight, right?
Author
Owner

@JohnEstropia commented on GitHub (Jun 11, 2017):

@athei Yes! There are two ways you can do this, let's say you want to rename a name property into fullName.

/// old model
class Person: CoreStoreObject {
    let name = Value.Required<String>("name")
}

Method 1: won't require any migration at all:

You just rename the Swift property and keep the same string key:

/// new model
class Person: CoreStoreObject {
    let fullName = Value.Required<String>("name")
}

This won't trigger any migration because the persistent store is exactly the same. All you changed is the swift label name.

Method 2: Lightweight migration

This method will require you to create a new model version. The Value.Required, Value.Optional, etc. all have a renamingIdentifier: argument you can provide:

enum V1 { // old model
    class Person: CoreStoreObject {
        let name = Value.Required<String>("name")
            // renamingIdentifier defaults to the same value as the attribute name if not provided
    }
}
enum V2 { // new model
    class Person: CoreStoreObject {
        let name = Value.Required<String>("fullname", renamingIdentifier: "name")
    }
}

// ...
typealias Person = V2.Person
// I recommend doing this. Very convenient way to manage model versions in code.

// ...
let dataStack = DataStack(
    schemaHistory: SchemaHistory(
        CoreStoreSchema(
            modelVersion: "V1",
            entities: [
                Entity<V1.Person>("Person")
            ],
            versionLock: [
                "Person": [0x66d8bbfd8b21561f, 0xcecec69ecae3570f, 0xc4b73d71256214ef, 0x89b99bfe3e013e8b]
            ]
        ),
        CoreStoreSchema(
            modelVersion: "V2",
            entities: [
                Entity<V2.Person>("Person")
            ],
            versionLock: [
                "Person": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a]
            ]
        ),
        migrationChain: ["V1", "V2"]
    )
)
dataStack.addStorage(
    SQLiteStore(...)),
        // No need to provide migrationMappingProviders.
        // CoreStore automatically tries InferredSchemaMappingProvider (which falls back to lightweight migration) when no other mapping providers are provided
    // ...
)
@JohnEstropia commented on GitHub (Jun 11, 2017): @athei Yes! There are two ways you can do this, let's say you want to rename a `name` property into `fullName`. ```swift /// old model class Person: CoreStoreObject { let name = Value.Required<String>("name") } ``` ### Method 1: won't require any migration at all: You just rename the Swift property and keep the same string key: ```swift /// new model class Person: CoreStoreObject { let fullName = Value.Required<String>("name") } ``` This won't trigger any migration because the persistent store is exactly the same. All you changed is the swift label name. ### Method 2: Lightweight migration This method will require you to create a new model version. The `Value.Required`, `Value.Optional`, etc. all have a `renamingIdentifier:` argument you can provide: ```swift enum V1 { // old model class Person: CoreStoreObject { let name = Value.Required<String>("name") // renamingIdentifier defaults to the same value as the attribute name if not provided } } enum V2 { // new model class Person: CoreStoreObject { let name = Value.Required<String>("fullname", renamingIdentifier: "name") } } // ... typealias Person = V2.Person // I recommend doing this. Very convenient way to manage model versions in code. // ... let dataStack = DataStack( schemaHistory: SchemaHistory( CoreStoreSchema( modelVersion: "V1", entities: [ Entity<V1.Person>("Person") ], versionLock: [ "Person": [0x66d8bbfd8b21561f, 0xcecec69ecae3570f, 0xc4b73d71256214ef, 0x89b99bfe3e013e8b] ] ), CoreStoreSchema( modelVersion: "V2", entities: [ Entity<V2.Person>("Person") ], versionLock: [ "Person": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a] ] ), migrationChain: ["V1", "V2"] ) ) dataStack.addStorage( SQLiteStore(...)), // No need to provide migrationMappingProviders. // CoreStore automatically tries InferredSchemaMappingProvider (which falls back to lightweight migration) when no other mapping providers are provided // ... ) ```
Author
Owner

@athei commented on GitHub (Jun 11, 2017):

Thanks. Overlooked this constructor. My main incentive for using CoreStoreObject is to have an easier review process. I mean who wants to review model files.

@athei commented on GitHub (Jun 11, 2017): Thanks. Overlooked this constructor. My main incentive for using CoreStoreObject is to have an easier review process. I mean who wants to review model files.
Author
Owner

@JohnEstropia commented on GitHub (Jun 11, 2017):

That is true, that's why I wrote this feature 🙂
You might be able further shorten these "reviews" if you always provide the versionLock. Any changes to a "locked" schema will raise an assertion failure during CoreStoreSchema's init.

@JohnEstropia commented on GitHub (Jun 11, 2017): That is true, that's why I wrote this feature 🙂 You might be able further shorten these "reviews" if you always provide the [versionLock](https://github.com/JohnEstropia/CoreStore#versionlocks). Any changes to a "locked" schema will raise an assertion failure during `CoreStoreSchema`'s init.
Author
Owner

@mantas commented on GitHub (Jul 4, 2017):

Are there callbacks to support prepareForDeletion, willSaveetc? Checked the source code, but couldn't find anything similar.

@mantas commented on GitHub (Jul 4, 2017): Are there callbacks to support `prepareForDeletion`, `willSave`etc? Checked the source code, but couldn't find anything similar.
Author
Owner

@JohnEstropia commented on GitHub (Jul 4, 2017):

@mantas None right now. May I ask what use case you have for using those methods?

@JohnEstropia commented on GitHub (Jul 4, 2017): @mantas None right now. May I ask what use case you have for using those methods?
Author
Owner

@mantas commented on GitHub (Jul 4, 2017):

@JohnEstropia I use prepareForDeletion to clean up out-of-CoreData assets. awakeFromInsert can be partially replaced with default values, but I'm not sure if defaults will cut in all cases.

@mantas commented on GitHub (Jul 4, 2017): @JohnEstropia I use `prepareForDeletion` to clean up out-of-CoreData assets. `awakeFromInsert` can be partially replaced with default values, but I'm not sure if defaults will cut in all cases.
Author
Owner

@JohnEstropia commented on GitHub (Jul 5, 2017):

@mantas

out-of-CoreData assets

I see. How do you handle cases in case the save fails for that transaction?

I'll see if we can hook up an overridable method for those callbacks. Something like

class MyObject: CoreStoreObject
    func prepareForDeletion(in transaction: BaseDataTransaction)
    func willSave(in transaction: BaseDataTransaction)
@JohnEstropia commented on GitHub (Jul 5, 2017): @mantas > out-of-CoreData assets I see. How do you handle cases in case the save fails for that transaction? I'll see if we can hook up an overridable method for those callbacks. Something like ```swift class MyObject: CoreStoreObject func prepareForDeletion(in transaction: BaseDataTransaction) func willSave(in transaction: BaseDataTransaction) ```
Author
Owner

@mantas commented on GitHub (Jul 5, 2017):

@JohnEstropia that'd be perfect, at least in my use case

@mantas commented on GitHub (Jul 5, 2017): @JohnEstropia that'd be perfect, at least in my use case
Author
Owner

@JohnEstropia commented on GitHub (Jul 5, 2017):

@mantas Thanks, I'll check other NSManagedObject methods as well, there may be other useful callbacks there.

I'm still curious how you handle your out-of-CoreData resources in case the save fails. If you can share your workflow I'll consider that as well.

@JohnEstropia commented on GitHub (Jul 5, 2017): @mantas Thanks, I'll check other NSManagedObject methods as well, there may be other useful callbacks there. I'm still curious how you handle your out-of-CoreData resources in case the save fails. If you can share your workflow I'll consider that as well.
Author
Owner

@mantas commented on GitHub (Jul 5, 2017):

@JohnEstropia it's cached images. If they're deleted, but the record deletion fails, they'd be redownloaded on next use. In worst case (e.g. device is offline), a placeholder image would appear. Although it's not foolproof and theoretically may cause problems, it seems to work pretty well so far.

@mantas commented on GitHub (Jul 5, 2017): @JohnEstropia it's cached images. If they're deleted, but the record deletion fails, they'd be redownloaded on next use. In worst case (e.g. device is offline), a placeholder image would appear. Although it's not foolproof and theoretically may cause problems, it seems to work pretty well so far.
Author
Owner

@JohnEstropia commented on GitHub (Jul 5, 2017):

@mantas I see. Thanks. I'll see what I can come up with.
(Will probably involve some swizzling, as CoreStoreObject already does when custom property getters and setters are implemented.)

In the mean time, if you need this functionality now you can try to gather all your cache keys and return them from the transaction, then release those cache altogether from inside the completion block.

@JohnEstropia commented on GitHub (Jul 5, 2017): @mantas I see. Thanks. I'll see what I can come up with. (Will probably involve some swizzling, as CoreStoreObject already does when custom property getters and setters are implemented.) In the mean time, if you need this functionality now you can try to gather all your cache keys and return them from the transaction, then release those cache altogether from inside the completion block.
Author
Owner

@mantas commented on GitHub (Jul 5, 2017):

@JohnEstropia thank you

@mantas commented on GitHub (Jul 5, 2017): @JohnEstropia thank you
Author
Owner

@mantas commented on GitHub (Jul 5, 2017):

@JohnEstropia I bumped into another interesting feature. Following works fine:

let title = Value.Required<String>("title")

But this fails to compile on Swift 3.1/Xcode 8:

let modifiedAt = Value.Required<Date>("modifiedAt")

The later works only with default value. Although I'd like it to have no default value and fail to insert if no value was set. Is this behaviour due to CoreData limitations (although date fields are as native as strings) or a bug?

@mantas commented on GitHub (Jul 5, 2017): @JohnEstropia I bumped into another interesting feature. Following works fine: `let title = Value.Required<String>("title")` But this fails to compile on Swift 3.1/Xcode 8: `let modifiedAt = Value.Required<Date>("modifiedAt")` The later works only with default value. Although I'd like it to have no default value and fail to insert if no value was set. Is this behaviour due to CoreData limitations (although date fields are as native as strings) or a bug?
Author
Owner

@JohnEstropia commented on GitHub (Jul 5, 2017):

@mantas Some types require a default: argument. This is actually the designed behavior but I can't find a way to make the compiler give a better compiler error. The reason is that types like URL and UUID have no intuitive default (URL for example crashes on empty strings, unlike NSURL that is ok with it).

For date, we can opt to just use a 0-TimeInterval date but I thought using Value.Optional would be safer for that case, as I can't think of a case where storing a pseudo-null (0) is better than storing null.

I'm open to feedback though, I might be missing some reasonable uses.

@JohnEstropia commented on GitHub (Jul 5, 2017): @mantas Some types require a `default:` argument. This is actually the designed behavior but I can't find a way to make the compiler give a better compiler error. The reason is that types like `URL` and `UUID` have no intuitive default (`URL` for example crashes on empty strings, unlike `NSURL` that is ok with it). For date, we can opt to just use a 0-TimeInterval date but I thought using `Value.Optional` would be safer for that case, as I can't think of a case where storing a pseudo-null (0) is better than storing null. I'm open to feedback though, I might be missing some reasonable uses.
Author
Owner

@mantas commented on GitHub (Jul 5, 2017):

@JohnEstropia feel free to correct me if I'm wrong, but it looks like vanilla CoreData allows to require a date field while giving it no default value.

Using optional value when I know the value will be present doesn't feel right. Using dummy default value just to work around that is just asking for an accidental bug too.

With NSManagedObject, I usually just leave default as nil. If the attribute wasn't set before insertion, save would fail. Which is good. While I'd have clean code elsewhere in the app. If an attribute is optional, it may or may not be present, if it's non-optional, then it's always there and correct (rather than dummy default).

The reason is that types like URL and UUID have no intuitive default

Are other fields getting an intuitive default set? For example, if a required string attribute doesn't have any default, would it fail on insert or have empty string instead?

@mantas commented on GitHub (Jul 5, 2017): @JohnEstropia feel free to correct me if I'm wrong, but it looks like vanilla CoreData allows to require a date field while giving it no default value. Using optional value when I know the value will be present doesn't feel right. Using dummy default value just to work around that is just asking for an accidental bug too. With `NSManagedObject`, I usually just leave default as nil. If the attribute wasn't set before insertion, save would fail. Which is good. While I'd have clean code elsewhere in the app. If an attribute is optional, it may or may not be present, if it's non-optional, then it's always there and correct (rather than dummy default). > The reason is that types like URL and UUID have no intuitive default Are other fields getting an intuitive default set? For example, if a required string attribute doesn't have any default, would it fail on insert or have empty string instead?
Author
Owner

@JohnEstropia commented on GitHub (Jul 5, 2017):

@mantas

it looks like vanilla CoreData allows to require a date field while giving it no default value.

It is true that Core Data allows this, but the goal of CoreStoreObject is not to imitate NSManagedObject's broad functionality, but to interface the underlying data as close as possible to a type-safe Swift class (which requires all properties be initialized when created). In that sense, CoreStoreObject currently isn't designed for "invalid objects".

Using optional value when I know the value will be present doesn't feel right. Using dummy default value just to work around that is just asking for an accidental bug too.

The same could be said otherwise, though. Until you save the object, you have incomplete objects you can accidentally read values from. But I digress.

In hindsight, the naming I used is a bit confusing to experienced CoreData users. While Value.Optional have the same nuance as Value.Nullable, Value.Required might have been better expressed as Value.Nonnull.

With NSManagedObject, I usually just leave default as nil. If the attribute wasn't set before insertion, save would fail. Which is good. While I'd have clean code elsewhere in the app. If an attribute is optional, it may or may not be present, if it's non-optional, then it's always there and correct (rather than dummy default).

The best way to express this is through an implicitly-unwrapped optional: var value: V!. In Swift and Objective-C's sense, this would be Value.NullResettable. Note that accessing this property when nil will cause a crash. The only way you can get the current NSManagedObject behavior is still to use optional properties and to tell Core Data to do validation during save. So I think the choices are:

  • Create a property (Value.NullResettable?) that returns an implicitly-unwrapped optional
  • Add an isRequired: argument to Value.Optional

In my experience though, when invalid data is being expected most implementations explicitly validate those data before passing them to the model (e.g. ImportableObject) rather than relying on CoreData's validation. For example, I'd rather do this:

self.name = json["name"] as! String

or this:

guard let name = json["name"] as? String else {
    throw ValidationError("name")
}
self.name = name

to fail as close to where the problem happened than to process everything needlessly and wait for the CoreData's validation error.

Are other fields getting an intuitive default set? For example, if a required string attribute doesn't have any default, would it fail on insert or have empty string instead?

Right now it would save with an empty string (or 0 for numeric types). Like I said, the current "optional-required" is more of a "nullable-nonnull" paradigm.

@JohnEstropia commented on GitHub (Jul 5, 2017): @mantas > it looks like vanilla CoreData allows to require a date field while giving it no default value. It is true that Core Data allows this, but the goal of `CoreStoreObject` is not to imitate `NSManagedObject`'s broad functionality, but to interface the underlying data as close as possible to a type-safe Swift class (which requires all properties be initialized when created). In that sense, `CoreStoreObject` currently isn't designed for "invalid objects". > Using optional value when I know the value will be present doesn't feel right. Using dummy default value just to work around that is just asking for an accidental bug too. The same could be said otherwise, though. Until you save the object, you have incomplete objects you can accidentally read values from. But I digress. In hindsight, the naming I used is a bit confusing to experienced CoreData users. While `Value.Optional` have the same nuance as `Value.Nullable`, `Value.Required` might have been better expressed as `Value.Nonnull`. > With NSManagedObject, I usually just leave default as nil. If the attribute wasn't set before insertion, save would fail. Which is good. While I'd have clean code elsewhere in the app. If an attribute is optional, it may or may not be present, if it's non-optional, then it's always there and correct (rather than dummy default). The best way to express this is through an implicitly-unwrapped optional: `var value: V!`. In Swift and Objective-C's sense, this would be `Value.NullResettable`. Note that accessing this property when `nil` will cause a crash. The only way you can get the current `NSManagedObject` behavior is still to use optional properties and to tell Core Data to do validation during save. So I think the choices are: - Create a property (`Value.NullResettable`?) that returns an implicitly-unwrapped optional - Add an `isRequired:` argument to `Value.Optional` In my experience though, when invalid data is being expected most implementations explicitly validate those data before passing them to the model (e.g. `ImportableObject`) rather than relying on CoreData's validation. For example, I'd rather do this: ```swift self.name = json["name"] as! String ``` or this: ```swift guard let name = json["name"] as? String else { throw ValidationError("name") } self.name = name ``` to fail as close to where the problem happened than to process everything needlessly and wait for the CoreData's validation error. > Are other fields getting an intuitive default set? For example, if a required string attribute doesn't have any default, would it fail on insert or have empty string instead? Right now it would save with an empty string (or 0 for numeric types). Like I said, the current "optional-required" is more of a "nullable-nonnull" paradigm.
Author
Owner

@mantas commented on GitHub (Jul 6, 2017):

@JohnEstropia

I guess it boils down to where the code smell would end up

  • outside of model, knowing model may return unexpected default value that wouldn't be correct according to business logic
  • outside of model, unwrapping values that are non-optional according to business logic
  • in model, unwrapping values that are non-optional according to business logic (e.g. having custom getter that forces unwrap)
  • in initialisation of model - some properties would not be readable till they're set

Personally I prefer the 3rd case. Which is probably influenced by years with ObjC :) Although I agree it has disadvantages. Namely it may be not expected by looking at the surrounding code. Having an additional getter that force-unwraps such values may be better from code readability point of view?

Either way, I think having silent default is not intuitive, especially coming from CoreData. I'd suggest to require default value for all data types if you want to go this way. This would avoid a lot of confusion and make things much more clear.

Having Value.NullResettable would be nice. It could have a flag to check if it was set yet. Or a function that takes a block which receives an optional.

@mantas commented on GitHub (Jul 6, 2017): @JohnEstropia I guess it boils down to where the code smell would end up - outside of model, knowing model may return unexpected default value that wouldn't be correct according to business logic - outside of model, unwrapping values that are non-optional according to business logic - in model, unwrapping values that are non-optional according to business logic (e.g. having custom getter that forces unwrap) - in initialisation of model - some properties would not be readable till they're set Personally I prefer the 3rd case. Which is probably influenced by years with ObjC :) Although I agree it has disadvantages. Namely it may be not expected by looking at the surrounding code. Having an additional getter that force-unwraps such values may be better from code readability point of view? Either way, I think having silent default is not intuitive, especially coming from CoreData. I'd suggest to require default value for all data types if you want to go this way. This would avoid a lot of confusion and make things much more clear. Having `Value.NullResettable` would be nice. It could have a flag to check if it was set yet. Or a function that takes a block which receives an optional.
Author
Owner

@JohnEstropia commented on GitHub (Aug 3, 2017):

@mantas I prototyped NullResettable and frankly I did not like the complexity it introduced just to have a force-unwrappable getter. There're now these cases that we need to be aware of:

  • Value.Nonnull: always required, always non-null, defaults to empty if possible
  • Value.Nullable: not required, always nullable, defaults to nil
  • Value.NullResettable: may or may not be required, may or may not be nullable, default value is ambiguous wether nil or empty

So I decided to scratch the idea and stick to Value.Required and Value.Optional. To compensate for the confusion (the same issue was asked quite a few times in the Slack group), I'll delete the EmptyableAttributeType completely and just always require a default value for Value.Required:

let name = Value.Required<String>("name", initial: "") // renamed default to initial

(Will be available in the Swift 3.1 and Swift 4 branches)

While the extra parameter might be verbose, hopefully this is a good enough way to balance clarity and simplicity.

As for where the code smell would end up, it would be:

  • outside of model, leaving validation to a separate location (e.g. near JSON decoder layers, including ImportableObject)

In a future update, I'll think up an API for validation techniques close to (or better than) the current Core Data methods. Validations not just for nullability, but for value restrictions as well (e.g. min/max).

@JohnEstropia commented on GitHub (Aug 3, 2017): @mantas I prototyped `NullResettable` and frankly I did not like the complexity it introduced just to have a force-unwrappable getter. There're now these cases that we need to be aware of: - `Value.Nonnull`: always required, always non-null, defaults to empty if possible - `Value.Nullable`: not required, always nullable, defaults to nil - `Value.NullResettable`: may or may not be required, may or may not be nullable, default value is ambiguous wether nil or empty So I decided to scratch the idea and stick to `Value.Required` and `Value.Optional`. To compensate for the confusion (the same issue was asked quite a few times in the Slack group), I'll delete the `EmptyableAttributeType` completely and just always require a default value for `Value.Required`: ``` let name = Value.Required<String>("name", initial: "") // renamed default to initial ``` (Will be available in the Swift 3.1 and Swift 4 branches) While the extra parameter might be verbose, hopefully this is a good enough way to balance clarity and simplicity. As for *where the code smell would end up*, it would be: - outside of model, leaving validation to a separate location (e.g. near JSON decoder layers, including `ImportableObject`) In a future update, I'll think up an API for validation techniques close to (or better than) the current Core Data methods. Validations not just for nullability, but for value restrictions as well (e.g. min/max).
Author
Owner

@mantas commented on GitHub (Aug 6, 2017):

@JohnEstropia

Fair enough! Thank you for putting time into this.

@mantas commented on GitHub (Aug 6, 2017): @JohnEstropia Fair enough! Thank you for putting time into this.
Author
Owner

@akac commented on GitHub (Aug 16, 2017):

Is there a way to tell a Data property to use external storage? For example, I want to store images as part of the database, but externally.

In years past, we did this the "hard way" which was to manually store it in a folder and just put the name/path in the database. But with CoreData's automatic management externally for us, that saves us tons of code and the need for the prepareForDeletion, and so on.

@akac commented on GitHub (Aug 16, 2017): Is there a way to tell a Data property to use external storage? For example, I want to store images as part of the database, but externally. In years past, we did this the "hard way" which was to manually store it in a folder and just put the name/path in the database. But with CoreData's automatic management externally for us, that saves us tons of code and the need for the prepareForDeletion, and so on.
Author
Owner

@akac commented on GitHub (Aug 16, 2017):

Also, Is there a way to save a transformable dictionary or array? It would be a huge overkill to create a whole new record with relationships for these.

Answer: Transformable.

@akac commented on GitHub (Aug 16, 2017): Also, Is there a way to save a transformable dictionary or array? It would be a huge overkill to create a whole new record with relationships for these. Answer: Transformable.
Author
Owner

@JohnEstropia commented on GitHub (Aug 23, 2017):

@akac Ah, I completely missed your comment, sorry. You're right, we should have a way to set external storage on NSData-backed attributes. I'll see if I can hack an initializer for Value.Required/Optional<T where T.CoreDataNativeType: NSData> and Transformable.Required/Optional<T>

@JohnEstropia commented on GitHub (Aug 23, 2017): @akac Ah, I completely missed your comment, sorry. You're right, we should have a way to set external storage on `NSData`-backed attributes. I'll see if I can hack an initializer for `Value.Required/Optional<T where T.CoreDataNativeType: NSData>` and `Transformable.Required/Optional<T>`
Author
Owner

@zalogatomek commented on GitHub (Sep 8, 2017):

@JohnEstropia Is it posible to build OrderBy clause with CoreStoreObject's function on relationship's property?

I mean something like:

let orderBy = Person.orderBy(descending: { $0.dog.name }) // won't compile
let anotherOrderBy = Person.orderBy(descending: { $0.dog.value.name }) // also won't compile

@zalogatomek commented on GitHub (Sep 8, 2017): @JohnEstropia Is it posible to build OrderBy clause with CoreStoreObject's function on relationship's property? I mean something like: `let orderBy = Person.orderBy(descending: { $0.dog.name }) // won't compile` `let anotherOrderBy = Person.orderBy(descending: { $0.dog.value.name }) // also won't compile`
Author
Owner

@JohnEstropia commented on GitHub (Sep 8, 2017):

@zalogatomek Unfortunately, CoreData doesn't allow ordering on relationship keyPaths. You need to create an intermediate property on Person to cache its dog.name.

This is actually an improvement on vanilla CoreData where you can compile this but get a runtime error when the fetch is executed.

@JohnEstropia commented on GitHub (Sep 8, 2017): @zalogatomek Unfortunately, CoreData doesn't allow ordering on relationship keyPaths. You need to create an intermediate property on `Person` to cache its `dog.name`. This is actually an improvement on vanilla CoreData where you can compile this but get a runtime error when the fetch is executed.
Author
Owner

@zalogatomek commented on GitHub (Sep 8, 2017):

@JohnEstropia Ok, you're right, OrderBy was bad example. Nevertheless, is it possible to do the same with Where or/and KeyPath? Like for example:

let whereClause = Person.where{ $0.dog.name == "Dog" } // or { $0.dog.value.name == "Dog" }
let keyPath = Person.keyPath{ $0.dog.name } // or { $0.dog.value.name }

I have been able to do this only like that:

let whereClause = Where("%K == %@", "dog.name", "Dog" )
@zalogatomek commented on GitHub (Sep 8, 2017): @JohnEstropia Ok, you're right, `OrderBy` was bad example. Nevertheless, is it possible to do the same with `Where` or/and `KeyPath`? Like for example: ```swift let whereClause = Person.where{ $0.dog.name == "Dog" } // or { $0.dog.value.name == "Dog" } let keyPath = Person.keyPath{ $0.dog.name } // or { $0.dog.value.name } ``` I have been able to do this only like that: ```swift let whereClause = Where("%K == %@", "dog.name", "Dog" ) ```
Author
Owner

@JohnEstropia commented on GitHub (Sep 9, 2017):

@zalogatomek You are right, the only way to do that now is to rely on raw string predicates. I've been meaning to create a compile-time utility for relationship keyPaths, as well as common aggregates (count, max, etc) but haven't found a good syntax yet.

The way attribute containers (Value.Required, Relationship.ToMany etc.) are structured, we can't simply chain properties like this. Swift 4 keypaths will definitely help, but we'll definitely need some macro-like function or operator.

Let me know if you have ideas.

@JohnEstropia commented on GitHub (Sep 9, 2017): @zalogatomek You are right, the only way to do that now is to rely on raw string predicates. I've been meaning to create a compile-time utility for relationship keyPaths, as well as common aggregates (count, max, etc) but haven't found a good syntax yet. The way attribute containers (Value.Required, Relationship.ToMany etc.) are structured, we can't simply chain properties like this. Swift 4 keypaths will definitely help, but we'll definitely need some macro-like function or operator. Let me know if you have ideas.
Author
Owner

@zalogatomek commented on GitHub (Oct 6, 2017):

@JohnEstropia Let's assume some simple model:

class Animal: CoreStoreObject { // isAbstract = true
    // body
}

final class Dog: Animal {
    // body
}

final class Cat: Animal {
    // body
}

Now, If I'm fetching from Animal, or using ListMonitor<Animal> is it possible to get real entity subclass? Like for example:

let animals = CoreStore.fetchAll(From<Animal>())
let dogs = animals.filter{ $0 is Dog }   // always return empty array

or:

let animals = CoreStore.fetchAll(From<Animal>())

if let dog = animals.first as? Dog {
    // never
} else if let cat = animals.first as? Cat {
    // never
} else {
    // always
}

Both examples would work, if Animal was NSManagedObject's subclass.

The only way I could do it was:

let animals = CoreStore.fetchAll(From<Animal>())

let rawAnimal = animals.first?.cs_toRaw()

if rawAnimal.entity.name == String(describing: Dog) {
    // it's a Dog
    let dog = CoreStore.fetchOne(From<Dog>(), Where("SELF == %@", rawAnimal.cs_id()))!
    
} else rawAnimal.entity.name == String(describing: Cat)  {
    // it's a Cat
} else {
    // never
}

But this of course seems wrong in so many ways, even in docs of cs_toRaw() and cs_id() functions, which says Do not call directly.

@zalogatomek commented on GitHub (Oct 6, 2017): @JohnEstropia Let's assume some simple model: ```swift class Animal: CoreStoreObject { // isAbstract = true // body } final class Dog: Animal { // body } final class Cat: Animal { // body } ``` Now, If I'm fetching from `Animal`, or using `ListMonitor<Animal>` is it possible to get real entity subclass? Like for example: ```swift let animals = CoreStore.fetchAll(From<Animal>()) let dogs = animals.filter{ $0 is Dog } // always return empty array ``` or: ```swift let animals = CoreStore.fetchAll(From<Animal>()) if let dog = animals.first as? Dog { // never } else if let cat = animals.first as? Cat { // never } else { // always } ``` Both examples would work, if Animal was NSManagedObject's subclass. The only way I could do it was: ```swift let animals = CoreStore.fetchAll(From<Animal>()) let rawAnimal = animals.first?.cs_toRaw() if rawAnimal.entity.name == String(describing: Dog) { // it's a Dog let dog = CoreStore.fetchOne(From<Dog>(), Where("SELF == %@", rawAnimal.cs_id()))! } else rawAnimal.entity.name == String(describing: Cat) { // it's a Cat } else { // never } ``` But this of course seems wrong in so many ways, even in docs of `cs_toRaw()` and `cs_id()` functions, which says _Do not call directly._
Author
Owner

@JohnEstropia commented on GitHub (Oct 6, 2017):

@zalogatomek I was pretty sure this was addressed before, I realized this was fixed only on the Swift 4 branch: https://github.com/JohnEstropia/CoreStore/pull/191

Thanks for raising this, will check on the master and Swift 3.2 branches.

@JohnEstropia commented on GitHub (Oct 6, 2017): @zalogatomek I was pretty sure this was addressed before, I realized this was fixed only on the Swift 4 branch: https://github.com/JohnEstropia/CoreStore/pull/191 Thanks for raising this, will check on the master and Swift 3.2 branches.
Author
Owner

@zalogatomek commented on GitHub (Oct 6, 2017):

@JohnEstropia with prototype/swift_4_0 branch it works as expected, thanks!

@zalogatomek commented on GitHub (Oct 6, 2017): @JohnEstropia with prototype/swift_4_0 branch it works as expected, thanks!
Author
Owner

@sebkasanzew commented on GitHub (Oct 10, 2017):

Not sure if this is the right place to ask.
I'm trying to implement ImportableUniqueObject on a CoreStoreObject.

class SessionEntity: CoreStoreObject, ImportableUniqueObject {
  let sessionID = Value.Required<UUID>( "sessionID", initial: UUID() )
  // other properties ...

  // MARK: ImportableUniqueObject
  typealias ImportSource = [String : Any]
  
  class var uniqueIDKeyPath: String {
    return #keyPath( SessionEntity.sessionID ) // Argument of '#keyPath' refers to non-'@objc' property 'sessionID'
  }

  var uniqueIDValue: UUID {
    get { return self.sessionID.value }
    set { self.sessionID.value = newValue }
  }

  class func uniqueID( from source: ImportSource, in transaction: BaseDataTransaction ) throws -> UUID? {
    return source["sessionID"] as? UUID
  }
}

I get an error in the property for the #keyPath stating:

Argument of '#keyPath' refers to non-'@objc' property 'sessionID'

What am I missing?
I'm using the branch prototype/Swift_4_0

@sebkasanzew commented on GitHub (Oct 10, 2017): Not sure if this is the right place to ask. I'm trying to implement `ImportableUniqueObject` on a CoreStoreObject. ```swift class SessionEntity: CoreStoreObject, ImportableUniqueObject { let sessionID = Value.Required<UUID>( "sessionID", initial: UUID() ) // other properties ... // MARK: ImportableUniqueObject typealias ImportSource = [String : Any] class var uniqueIDKeyPath: String { return #keyPath( SessionEntity.sessionID ) // Argument of '#keyPath' refers to non-'@objc' property 'sessionID' } var uniqueIDValue: UUID { get { return self.sessionID.value } set { self.sessionID.value = newValue } } class func uniqueID( from source: ImportSource, in transaction: BaseDataTransaction ) throws -> UUID? { return source["sessionID"] as? UUID } } ``` I get an error in the property for the #keyPath stating: > Argument of '#keyPath' refers to non-'@objc' property 'sessionID' **What am I missing?** I'm using the branch `prototype/Swift_4_0`
Author
Owner

@JohnEstropia commented on GitHub (Oct 10, 2017):

@Sebkasanzew Hi, Swift's #keyPath syntax can only be used on Objective-C properties.
In our case, you can use

  class var uniqueIDKeyPath: String {
    return SessionEntity.keyPath { $0.sessionID }
  }

Which is CoreStore's designated syntax for Value.Required etc.

@JohnEstropia commented on GitHub (Oct 10, 2017): @Sebkasanzew Hi, Swift's `#keyPath` syntax can only be used on Objective-C properties. In our case, you can use ```swift class var uniqueIDKeyPath: String { return SessionEntity.keyPath { $0.sessionID } } ``` Which is CoreStore's designated syntax for `Value.Required` etc.
Author
Owner

@SlinToWin commented on GitHub (Oct 18, 2017):

Is there a way to define relationships of a Model A on different Models with CoreStoreObject?
At the Moment, i have to define a proxy object for every relationship because i have to define the inverse. Is there a way that inverse is optional?

Use Case:

Image
master = Relationship.ToOne<???>("master")

ModelA
image = Relationship.ToOne<Image>("image", inverse: { $0.master})

ModelB
image = Relationship.ToOne<Image>("image", inverse: { $0.master})

This won't work because we have to define the inverse of a specific type.
Making inverse optional (for unidirectional relationships) would be awesome

@SlinToWin commented on GitHub (Oct 18, 2017): Is there a way to define relationships of a Model A on different Models with CoreStoreObject? At the Moment, i have to define a proxy object for every relationship because i have to define the inverse. Is there a way that inverse is optional? Use Case: ``` Image master = Relationship.ToOne<???>("master") ModelA image = Relationship.ToOne<Image>("image", inverse: { $0.master}) ModelB image = Relationship.ToOne<Image>("image", inverse: { $0.master}) ``` This won't work because we have to define the inverse of a specific type. Making inverse optional (for unidirectional relationships) would be awesome
Author
Owner

@JohnEstropia commented on GitHub (Oct 19, 2017):

@SlinToWin CoreData doesn't recommend weak (one-way) relationships as it breaks the notification mechanism, and ultimately your object graph.

My recommendation here is to use two separate properties on Image, one for ModelA's use and one for ModelB's.

@JohnEstropia commented on GitHub (Oct 19, 2017): @SlinToWin CoreData doesn't recommend weak (one-way) relationships as it breaks the notification mechanism, and ultimately your object graph. My recommendation here is to use two separate properties on `Image`, one for ModelA's use and one for ModelB's.
Author
Owner

@SlinToWin commented on GitHub (Oct 19, 2017):

Works like a charm - thanks for your awesome library

@SlinToWin commented on GitHub (Oct 19, 2017): Works like a charm - thanks for your awesome library
Author
Owner

@kevinrenskers commented on GitHub (Oct 26, 2017):

I recently refactored a sizable app to switch from xcdatamodeld to CoreStoreObject. Most of it is very awesome, however the only thing that raised eyebrows in the team was that all property values now have to be accessed via .value. This made the diff very large, is less handy to work with, and worse, breaks other use cases. For example, we also use ObjectMapper to transform object from and to JSON and mapping back to JSON no longer works.

For example, our Brand model had a mapping function like this (before the switch to CoreStoreObject):

public func mapping(map: Map) {
    brandId <- map["id"]
    name <- map["name"]
    logoUrl <- map["logo"]
}

This is also used by our importUnqiueObject flow, all works fine. Mapping an object back to JSON also worked great. However, now with the switch to CoreStoreObjct this mapping function had to be changed to this:

public func mapping(map: Map) {
    brandId.value <- map["id"]
    name.value <- map["name"]
    logoUrl.value <- map["logo"]
}

But now it can only be used to map from JSON; it's impossible to map back to JSON since the properties are no longer simply String or Int or whatever (which is what ObjectMapper needs it to be); they are now these Value boxes. Worse, it crashes with the error that you can't assign a value outside of a transaction.

I have no clue how these problems could be solved, if at all, but that's my piece of feedback other then "it's pretty damn awesome!" :)

@kevinrenskers commented on GitHub (Oct 26, 2017): I recently refactored a sizable app to switch from `xcdatamodeld ` to CoreStoreObject. Most of it is very awesome, however the only thing that raised eyebrows in the team was that all property values now have to be accessed via `.value`. This made the diff very large, is less handy to work with, and worse, breaks other use cases. For example, we also use ObjectMapper to transform object from and to JSON and mapping back to JSON no longer works. For example, our Brand model had a mapping function like this (before the switch to CoreStoreObject): ``` public func mapping(map: Map) { brandId <- map["id"] name <- map["name"] logoUrl <- map["logo"] } ``` This is also used by our importUnqiueObject flow, all works fine. Mapping an object back to JSON also worked great. However, now with the switch to CoreStoreObjct this mapping function had to be changed to this: ``` public func mapping(map: Map) { brandId.value <- map["id"] name.value <- map["name"] logoUrl.value <- map["logo"] } ``` But now it can only be used to map _from_ JSON; it's impossible to map back _to_ JSON since the properties are no longer simply String or Int or whatever (which is what ObjectMapper needs it to be); they are now these Value boxes. Worse, it crashes with the error that you can't assign a value outside of a transaction. I have no clue how these problems could be solved, if at all, but that's my piece of feedback other then "it's pretty damn awesome!" :)
Author
Owner

@mantas commented on GitHub (Oct 26, 2017):

@kevinrenskers you could extend the operator to mimic the old syntax

@mantas commented on GitHub (Oct 26, 2017): @kevinrenskers you could extend the operator to mimic the old syntax
Author
Owner

@kevinrenskers commented on GitHub (Oct 26, 2017):

Actually, one other thought: it would be great to support Codable so we can drop ObjectMapper as a dependency (you already noted in another ticket that's it's on your list, but good to have it in this thread too). It would still be nice not to have to constantly .value though 😛

@kevinrenskers commented on GitHub (Oct 26, 2017): Actually, one other thought: it would be great to support Codable so we can drop ObjectMapper as a dependency (you already noted in another ticket that's it's on your list, but good to have it in this thread too). It would still be nice not to have to constantly `.value` though 😛
Author
Owner

@kevinrenskers commented on GitHub (Oct 26, 2017):

@mantas That sounds intriguing! How would I go about doing that? My Swift knowledge stops just short of knowing where to begin with that.

@kevinrenskers commented on GitHub (Oct 26, 2017): @mantas That sounds intriguing! How would I go about doing that? My Swift knowledge stops just short of knowing where to begin with that.
Author
Owner

@mantas commented on GitHub (Oct 26, 2017):

@kevinrenskers beware of semi pseudo code. See this great article on Swift operators. Syntax may have changed slightly since then.

func <- <T> (lhs: Value<T>, rhs: T){
  lhs.value = rhs
}

@mantas commented on GitHub (Oct 26, 2017): @kevinrenskers beware of semi pseudo code. See [this](http://nshipster.com/swift-operators/) great article on Swift operators. Syntax may have changed slightly since then. ``` func <- <T> (lhs: Value<T>, rhs: T){ lhs.value = rhs } ```
Author
Owner

@kevinrenskers commented on GitHub (Oct 26, 2017):

Ah right, but that's not really what I am after. The <- in the mapping function already comes from ObjectMapper, and I am mostly talking about having to use .value all throughout the app, everywhere where you use the properties.

@kevinrenskers commented on GitHub (Oct 26, 2017): Ah right, but that's not really what I am after. The `<-` in the mapping function already comes from ObjectMapper, and I am mostly talking about having to use `.value` all throughout the app, everywhere where you use the properties.
Author
Owner

@mantas commented on GitHub (Oct 26, 2017):

@kevinrenskers I get it. Swift operators is basically a function that takes left-hand and right-hand arguments. You could refactor A.value = B to A = B using custom operator:

func =-= (a: A, b: B) {
  a.value = b
}

a =-= b
a.value // b
@mantas commented on GitHub (Oct 26, 2017): @kevinrenskers I get it. Swift operators is basically a function that takes left-hand and right-hand arguments. You could refactor `A.value = B` to `A = B` using custom operator: ``` func =-= (a: A, b: B) { a.value = b } a =-= b a.value // b ```
Author
Owner

@JohnEstropia commented on GitHub (Oct 27, 2017):

@kevinrenskers Thanks for the feedback! As mantas already mentioned, I think your best bet here is custom operators. CoreStore already provides a few:

if person.name .== "John" {
    // ...
}
person.age .= 30
person.pet .= dog // relationship

For your ObjectMapper use, I think an overload would be enough:

static func <- <O: CoreStoreObject, T: ObjectMapper.BaseMappable>(lhs: O.Value.Required<T>, rhs: ObjectMapper.Map) {
    lhs.value <- rhs
}
// overload for O.Value.Optional<T>, O.Transformable.Required<T>, etc

Personally, I'd just use ImportableUniqueObject because transactions optimize uniquing by ID during import.

For the rest of your code, I do understand that the current syntax is more verbose. But I think that's the best we can do for now until Swift adds syntactic metadata (like Java annotations). If you have ideas, feel free to suggest :)

@JohnEstropia commented on GitHub (Oct 27, 2017): @kevinrenskers Thanks for the feedback! As mantas already mentioned, I think your best bet here is custom operators. CoreStore already provides a few: ```swift if person.name .== "John" { // ... } person.age .= 30 person.pet .= dog // relationship ``` For your ObjectMapper use, I think an overload would be enough: ```swift static func <- <O: CoreStoreObject, T: ObjectMapper.BaseMappable>(lhs: O.Value.Required<T>, rhs: ObjectMapper.Map) { lhs.value <- rhs } // overload for O.Value.Optional<T>, O.Transformable.Required<T>, etc ``` Personally, I'd just use `ImportableUniqueObject` because transactions optimize uniquing by ID during import. For the rest of your code, I do understand that the current syntax is more verbose. But I think that's the best we can do for now until Swift adds syntactic metadata (like Java annotations). If you have ideas, feel free to suggest :)
Author
Owner

@kevinrenskers commented on GitHub (Oct 27, 2017):

Oh I do use ImportableUniqueObject! The mapping is done in the update delegate method.

@kevinrenskers commented on GitHub (Oct 27, 2017): Oh I do use ImportableUniqueObject! The mapping is done in the update delegate method.
Author
Owner

@kevinrenskers commented on GitHub (Oct 29, 2017):

So one idea I had is to have shadow properties with setters and getters. This would solve a lot of my problems, but sadly it's a chore to write.

class Brand: CoreStoreObject {
  private let _brandId = Value.Required<Int>("brandId", initial: 0, isIndexed: true)
  private let _name = Value.Required<String>("name", initial: "")
  private let _logoUrl = Value.Optional<String>("logoUrl")
}

extension Brand: BaseMappable, ImportableUniqueObject {

  var brandId: Int {
    get {
      return _brandId.value
    }
    set(value) {
      _brandId.value = value
    }
  }

  var name: String {
    get {
      return _name.value
    }
    set(value) {
      _name.value = value
    }
  }

  var logoUrl: String? {
    get {
      return _logoUrl.value
    }
    set(value) {
      _logoUrl.value = value
    }
  }

  // rest of implementation...

Does anyone have an idea how to generate this in an easier DRYer way?

@kevinrenskers commented on GitHub (Oct 29, 2017): So one idea I had is to have shadow properties with setters and getters. This would solve a lot of my problems, but sadly it's a chore to write. ``` class Brand: CoreStoreObject { private let _brandId = Value.Required<Int>("brandId", initial: 0, isIndexed: true) private let _name = Value.Required<String>("name", initial: "") private let _logoUrl = Value.Optional<String>("logoUrl") } extension Brand: BaseMappable, ImportableUniqueObject { var brandId: Int { get { return _brandId.value } set(value) { _brandId.value = value } } var name: String { get { return _name.value } set(value) { _name.value = value } } var logoUrl: String? { get { return _logoUrl.value } set(value) { _logoUrl.value = value } } // rest of implementation... ``` Does anyone have an idea how to generate this in an easier DRYer way?
Author
Owner

@JohnEstropia commented on GitHub (Oct 29, 2017):

That's ok if you are just accessing and mutating properties, but you'll lose the ability to fetch with keypaths

@JohnEstropia commented on GitHub (Oct 29, 2017): That's ok if you are just accessing and mutating properties, but you'll lose the ability to fetch with keypaths
Author
Owner

@kevinrenskers commented on GitHub (Oct 29, 2017):

True, and it doesn't solve my .toJSON problem either, so not sure it's worth it just to not write .value. I don't think so :)

@kevinrenskers commented on GitHub (Oct 29, 2017): True, and it doesn't solve my `.toJSON` problem either, so not sure it's worth it just to not write `.value`. I don't think so :)
Author
Owner

@mantas commented on GitHub (Oct 29, 2017):

@kevinrenskers what's your .toJSON problem?

@mantas commented on GitHub (Oct 29, 2017): @kevinrenskers what's your `.toJSON` problem?
Author
Owner

@kevinrenskers commented on GitHub (Oct 29, 2017):

It's the problem I described a few comments back, where ObjectMapper's toJSON method is incompatible with CoreStoreObject because it's trying to mutate properties outside of a transaction. It creates a mutable copy of the object and then runs that mapping function on it, which assigns to .value.

It's not a huge deal, it's just that ObjectMapper is (partly) incompatible with CoreStoreObject because the first mutates properties and the second has an assert to make sure it doesn't happen outside of a transaction. I think that the toJSON method of ObjectMapper shouldn't be doing things in the way it does so I'll see if they can come up with a different solution to map objects back to JSON.

Basically ObjectMapper expects primitive objects (String, Int, etc) that it can just mutate at will.

@kevinrenskers commented on GitHub (Oct 29, 2017): It's the problem I described a few comments back, where ObjectMapper's `toJSON` method is incompatible with CoreStoreObject because it's trying to mutate properties outside of a transaction. It creates a mutable copy of the object and then runs that `mapping` function on it, which assigns to `.value`. It's not a huge deal, it's just that ObjectMapper is (partly) incompatible with CoreStoreObject because the first mutates properties and the second has an assert to make sure it doesn't happen outside of a transaction. I think that the `toJSON` method of ObjectMapper shouldn't be doing things in the way it does so I'll see if they can come up with a different solution to map objects back to JSON. Basically ObjectMapper expects primitive objects (String, Int, etc) that it can just mutate at will.
Author
Owner

@mantas commented on GitHub (Oct 29, 2017):

@kevinrenskers ahh, sorry about missing that. I'd use an additional struct to act as a proxy between ObjectMapper and the model. ObjectMapper loves to do quite a bit of magic, for better or worse...

Look into Swift 4.0 native JSON serialisation. It's really sweet. I haven't used it with CoreStoreObject yet, but I don't see why it wouldn't work.

@mantas commented on GitHub (Oct 29, 2017): @kevinrenskers ahh, sorry about missing that. I'd use an additional struct to act as a proxy between ObjectMapper and the model. ObjectMapper loves to do quite a bit of magic, for better or worse... Look into Swift 4.0 native JSON serialisation. It's really sweet. I haven't used it with CoreStoreObject yet, but I don't see why it wouldn't work.
Author
Owner

@kevinrenskers commented on GitHub (Oct 29, 2017):

We're not quite on Swift 4 yet but it's definitely on my radar :)

@kevinrenskers commented on GitHub (Oct 29, 2017): We're not quite on Swift 4 yet but it's definitely on my radar :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#147