mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-01-11 20:00:30 +01:00
Accepting feedback on the new CoreStoreObject system
#147
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
CoreStoreObjectmore or less is as functional asNSManagedObjectin common scenarios, but I might be missing some use-cases so I would like to hear everyone's opinion.@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 usingCustomSchemaMappingProviderto define the mapping its always heavyweight, right?@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
nameproperty intofullName.Method 1: won't require any migration at all:
You just rename the Swift property and keep the same string key:
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 arenamingIdentifier:argument you can provide:@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.
@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.@mantas commented on GitHub (Jul 4, 2017):
Are there callbacks to support
prepareForDeletion,willSaveetc? Checked the source code, but couldn't find anything similar.@JohnEstropia commented on GitHub (Jul 4, 2017):
@mantas None right now. May I ask what use case you have for using those methods?
@mantas commented on GitHub (Jul 4, 2017):
@JohnEstropia I use
prepareForDeletionto clean up out-of-CoreData assets.awakeFromInsertcan be partially replaced with default values, but I'm not sure if defaults will cut in all cases.@JohnEstropia commented on GitHub (Jul 5, 2017):
@mantas
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
@mantas commented on GitHub (Jul 5, 2017):
@JohnEstropia that'd be perfect, at least in my use case
@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.
@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.
@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.
@mantas commented on GitHub (Jul 5, 2017):
@JohnEstropia thank you
@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?
@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 likeURLandUUIDhave no intuitive default (URLfor example crashes on empty strings, unlikeNSURLthat is ok with it).For date, we can opt to just use a 0-TimeInterval date but I thought using
Value.Optionalwould 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.
@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).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?
@JohnEstropia commented on GitHub (Jul 5, 2017):
@mantas
It is true that Core Data allows this, but the goal of
CoreStoreObjectis not to imitateNSManagedObject'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,CoreStoreObjectcurrently isn't designed for "invalid objects".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.Optionalhave the same nuance asValue.Nullable,Value.Requiredmight have been better expressed asValue.Nonnull.The best way to express this is through an implicitly-unwrapped optional:
var value: V!. In Swift and Objective-C's sense, this would beValue.NullResettable. Note that accessing this property whennilwill cause a crash. The only way you can get the currentNSManagedObjectbehavior is still to use optional properties and to tell Core Data to do validation during save. So I think the choices are:Value.NullResettable?) that returns an implicitly-unwrapped optionalisRequired:argument toValue.OptionalIn 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:or this:
to fail as close to where the problem happened than to process everything needlessly and wait for the CoreData's validation error.
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.
@mantas commented on GitHub (Jul 6, 2017):
@JohnEstropia
I guess it boils down to where the code smell would end up
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.NullResettablewould 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.@JohnEstropia commented on GitHub (Aug 3, 2017):
@mantas I prototyped
NullResettableand 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 possibleValue.Nullable: not required, always nullable, defaults to nilValue.NullResettable: may or may not be required, may or may not be nullable, default value is ambiguous wether nil or emptySo I decided to scratch the idea and stick to
Value.RequiredandValue.Optional. To compensate for the confusion (the same issue was asked quite a few times in the Slack group), I'll delete theEmptyableAttributeTypecompletely and just always require a default value forValue.Required:(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:
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).
@mantas commented on GitHub (Aug 6, 2017):
@JohnEstropia
Fair enough! Thank you for putting time into this.
@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):
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.
@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 forValue.Required/Optional<T where T.CoreDataNativeType: NSData>andTransformable.Required/Optional<T>@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 compilelet anotherOrderBy = Person.orderBy(descending: { $0.dog.value.name }) // also won't compile@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
Personto cache itsdog.name.This is actually an improvement on vanilla CoreData where you can compile this but get a runtime error when the fetch is executed.
@zalogatomek commented on GitHub (Sep 8, 2017):
@JohnEstropia Ok, you're right,
OrderBywas bad example. Nevertheless, is it possible to do the same withWhereor/andKeyPath? Like for example:I have been able to do this only like that:
@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.
@zalogatomek commented on GitHub (Oct 6, 2017):
@JohnEstropia Let's assume some simple model:
Now, If I'm fetching from
Animal, or usingListMonitor<Animal>is it possible to get real entity subclass? Like for example:or:
Both examples would work, if Animal was NSManagedObject's subclass.
The only way I could do it was:
But this of course seems wrong in so many ways, even in docs of
cs_toRaw()andcs_id()functions, which says Do not call directly.@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.
@zalogatomek commented on GitHub (Oct 6, 2017):
@JohnEstropia with prototype/swift_4_0 branch it works as expected, thanks!
@sebkasanzew commented on GitHub (Oct 10, 2017):
Not sure if this is the right place to ask.
I'm trying to implement
ImportableUniqueObjecton a CoreStoreObject.I get an error in the property for the #keyPath stating:
What am I missing?
I'm using the branch
prototype/Swift_4_0@JohnEstropia commented on GitHub (Oct 10, 2017):
@Sebkasanzew Hi, Swift's
#keyPathsyntax can only be used on Objective-C properties.In our case, you can use
Which is CoreStore's designated syntax for
Value.Requiredetc.@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:
This won't work because we have to define the inverse of a specific type.
Making inverse optional (for unidirectional relationships) would be awesome
@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.@SlinToWin commented on GitHub (Oct 19, 2017):
Works like a charm - thanks for your awesome library
@kevinrenskers commented on GitHub (Oct 26, 2017):
I recently refactored a sizable app to switch from
xcdatamodeldto 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):
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:
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!" :)
@mantas commented on GitHub (Oct 26, 2017):
@kevinrenskers you could extend the operator to mimic the old syntax
@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
.valuethough 😛@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.
@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.
@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.valueall throughout the app, everywhere where you use the properties.@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 = BtoA = Busing custom operator:@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:
For your ObjectMapper use, I think an overload would be enough:
Personally, I'd just use
ImportableUniqueObjectbecause 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 :)
@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 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.
Does anyone have an idea how to generate this in an easier DRYer way?
@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
@kevinrenskers commented on GitHub (Oct 29, 2017):
True, and it doesn't solve my
.toJSONproblem either, so not sure it's worth it just to not write.value. I don't think so :)@mantas commented on GitHub (Oct 29, 2017):
@kevinrenskers what's your
.toJSONproblem?@kevinrenskers commented on GitHub (Oct 29, 2017):
It's the problem I described a few comments back, where ObjectMapper's
toJSONmethod 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 thatmappingfunction 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
toJSONmethod 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.
@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.
@kevinrenskers commented on GitHub (Oct 29, 2017):
We're not quite on Swift 4 yet but it's definitely on my radar :)