realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.32k stars 2.15k forks source link

Realm subproperty and NSPredicate #3004

Closed mhergon closed 8 years ago

mhergon commented 8 years ago

Hi,

I'm getting an error when I trying to make a NSPredicate with subproperty;

class InfoPoint: Object {
    dynamic var id = ""
    dynamic var name: String
    dynamic var location: LocationProperty?
}

and

class LocationProperty: Object {
    dynamic var lat: Double = 0.0
    dynamic var lng: Double = 0.0
}

When I execute this query:

let topLeftPredicate = NSPredicate(format: "%K <= %f AND %K >= %f", latitudeKey, box.topLeft.latitude, longitudeKey, box.topLeft.longitude)
let bottomRightPredicate = NSPredicate(format: "%K >= %f AND %K <= %f", latitudeKey, box.bottomRight.latitude, longitudeKey, box.bottomRight.longitude)
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [topLeftPredicate, bottomRightPredicate])

Results.filter(compoundPredicate)

I'm getting this error:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<RLMAccessor_v0_Marina 0x7fcf3967f270> valueForUndefinedKey:]: this class is not key value coding-compliant for the key location.lat.'

I tried with block version of NSPredicate but Realm doesn't support it. Any idea? Thanks!

bdash commented 8 years ago

The code as provided doesn't compile so I suspect you've edited it down from a larger example. Can you share a more complete example of what you're doing?

Here's a working test I threw together based on the code you provided:

class InfoPoint: Object {
    dynamic var id = ""
    dynamic var name: String = ""
    dynamic var location: LocationProperty?
}

class LocationProperty: Object {
    dynamic var lat: Double = 0.0
    dynamic var lng: Double = 0.0
}

let realm = Realm()

try! realm.write {
    realm.create(InfoPoint.self, value: ["LKPRT", "Lakeport, CA", [39.0431, -122.9158]])
    realm.create(InfoPoint.self, value: ["CHCH", "Christchurch, New Zealand", [-43.5300, 172.6203]])
}

var results = realm.objects(InfoPoint).filter("location.lat > %f AND location.lng < %f", 39, 120)
print("Found \(results.count) results")
print("The first result's ID is \(results.first!.id)")

results = realm.objects(InfoPoint).filter("location.lat < %f AND location.lng > %f", 0, 0);
print("Found \(results.count) results")
print("The first result's ID is \(results.first!.id)")
mhergon commented 8 years ago

@bdash I've updated the query code and I tried setting manually "location.lat" key and works fine. Only fails if I pass keys as variable like example

mhergon commented 8 years ago

@bdash After see your code, I'm tried this workaround:

let topLeftPredicate = NSPredicate(format: "\(latitudeKey) <= %f AND \(longitudeKey) >= %f", box.topLeft.latitude, box.topLeft.longitude)
let bottomRightPredicate = NSPredicate(format: "\(latitudeKey) >= %f AND \(longitudeKey) <= %f", box.bottomRight.latitude, box.bottomRight.longitude)

and works fine!

Thanks!

bdash commented 8 years ago

In your example you're calling Results.filter(predicate), but that won't compile since filter is an instance method rather than a class method. How are you really invoking filter?

Also, what are the values of latitudeKey and longitudeKey?

bdash commented 8 years ago

The change from %K to Swift string interpolation wouldn't fix the error you're hitting. I suspect you changed something else at the same time that really fixed the issue. That said, I'd suggest sticking to NSPredicate's interpolation syntax for this since it'll correctly handle the case where the keys you're substituting are keywords or operators.

mhergon commented 8 years ago

@bdash You can view my original code at this file: https://github.com/mhergon/RealmGeoQueries/blob/0aa53e46d81899357cf23fdf4f363d3361f03935/GeoQueries.swift

Query:

Realm().findNearby(Marina.self, origin: coordinate, radius: distance, sortAscending: true, latitudeKey: "location.lat", longitudeKey: "location.lng", distanceKey: "dist")

And I only changed to string interpolation, I promise ;)

bdash commented 8 years ago

The exception you were seeing was:

*\ Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<RLMAccessor_v0_Marina 0x7fcf3967f270> valueForUndefinedKey:]: this class is not key value coding-compliant for the key location.lat.'

This exception is being thrown by key-value coding machinery, typically called via -valueWithKeyPath:. Results.filter doesn't use KVC in its implementation so the exception cannot have come from calling Results.filter. That's why I think something else is going on.

mhergon commented 8 years ago

@bdash Yes, after some tests, keeps failing. Have you any workaround?

bdash commented 8 years ago

I'm still not able to reproduce the problem you're seeing. I went so far as to download your project from GitHub, build and run the example, modify it to contain a second model class with a link to the example's Point class, and update the query to specify longitudeKey and latitudeKey as you describe. No sign of the exception you mentioned.

Can you share a self-contained example that reproduces the problem? If that's not possible then can you provide more information about what the code you're using looks like? What do your model classes look like, for instance? The snippet you mentioned contains a reference to a Marina class that you've not shared the definition for.

bdash commented 8 years ago

From looking at GeoQueries.swift I see the following:

let location = CLLocation(latitude: obj.valueForKey(latitudeKey) as! CLLocationDegrees, longitude: obj.valueForKey(longitudeKey) as! CLLocationDegrees)

That looks like the culprit. valueForKey doesn't traverse key paths, so it tries to access "location.lat" as a property of obj. valueForKeyPath traverses key paths so it'll load the location property from obj, then use valueForKey to retrieve the lat property from it.

mhergon commented 8 years ago

@bdash The first exception occurs at "filterGeoBox:" method. So, above code has not executed...

I attached an example code with exception. Thanks! Subproperty Fails.zip

bdash commented 8 years ago

Thanks for the reproducible test case. When running the application I see an exception like so:

2015-12-15 12:44:53.672 Subproperty Fails[10245:440394] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<RLMAccessor_v0_Marina 0x7ff3ac805bd0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key location.lat.'
*** First throw call stack:
(
    0   CoreFoundation                      0x0000000101501e65 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x0000000103241deb objc_exception_throw + 48
    2   CoreFoundation                      0x0000000101501aa9 -[NSException raise] + 9
    3   Foundation                          0x000000010195d888 -[NSObject(NSKeyValueCoding) valueForUndefinedKey:] + 226
    4   Realm                               0x0000000100e15ae0 -[RLMObjectBase valueForUndefinedKey:] + 352
    5   Foundation                          0x00000001018b3997 -[NSObject(NSKeyValueCoding) valueForKey:] + 280
    6   Realm                               0x0000000100e1592d -[RLMObjectBase valueForKey:] + 237
    7   GeoQueries                          0x0000000100dab121 _TFFeRdq_C10RealmSwift6Object_10GeoQueriesCS_7Results15filterGeoRadiusuRdq_S0__FGS2_q__FTVSC22CLLocationCoordinate2D6radiusSd13sortAscendingGSqSb_11latitudeKeySS12longitudeKeySS11distanceKeySS_GSaq__U_FS0_Sb + 305
    8   GeoQueries                          0x0000000100daa9f7 _TPA__TFFeRdq_C10RealmSwift6Object_10GeoQueriesCS_7Results15filterGeoRadiusuRdq_S0__FGS2_q__FTVSC22CLLocationCoordinate2D6radiusSd13sortAscendingGSqSb_11latitudeKeySS12longitudeKeySS11distanceKeySS_GSaq__U_FS0_Sb + 263
    9   GeoQueries                          0x0000000100dab747 _TTRGRdq_C10RealmSwift6Object_XFo_oq__dSbzoPSs9ErrorType__XFo_iq__dSbzoPS1___ + 39
    10  libswiftCore.dylib                  0x000000010367f8c4 _TFeRq_Ss12SequenceType_SsS_6filteruRq_S__fq_FzFzqqq_S_9GeneratorSs13GeneratorType7ElementSbGSaqqq_S_9GeneratorS0_7Element_ + 468
    11  GeoQueries                          0x0000000100da9a6a _TFeRdq_C10RealmSwift6Object_10GeoQueriesCS_7Results15filterGeoRadiusuRdq_S0__fGS2_q__FTVSC22CLLocationCoordinate2D6radiusSd13sortAscendingGSqSb_11latitudeKeySS12longitudeKeySS11distanceKeySS_GSaq__ + 1162
    12  GeoQueries                          0x0000000100da9590 _TFE10GeoQueriesC10RealmSwift5Realm10findNearbyuRdq_CS0_6Object_fS1_FTMq_6originVSC22CLLocationCoordinate2D6radiusSd13sortAscendingGSqSb_11latitudeKeySS12longitudeKeySS11distanceKeySS_GSaq__ + 688
    13  Subproperty Fails                   0x0000000100c58ca5 _TFFC17Subproperty_Fails18MainViewController11viewDidLoadFS0_FT_T_U_FGSqCSo7NSError_T_ + 709
    14  Subproperty Fails                   0x0000000100c58de7 _TTRXFo_oGSqCSo7NSError__dT__XFo_iT5errorGSqS____iT__ + 23
    15  Subproperty Fails                   0x0000000100c57b31 _TPA__TTRXFo_oGSqCSo7NSError__dT__XFo_iT5errorGSqS____iT__ + 81
    16  Subproperty Fails                   0x0000000100c592ff _TFFC17Subproperty_Fails18MainViewController7marinasFS0_FGSqFT5errorGSqCSo7NSError__T__T_U_FTCSo20NSURLSessionDataTaskGSqPSs9AnyObject___T_ + 879
    17  Subproperty Fails                   0x0000000100c5cf43 _TTRXFo_oCSo20NSURLSessionDataTaskoGSqPSs9AnyObject___dT__XFo_iTS_GSqPS0_____iT__ + 35
    18  Subproperty Fails                   0x0000000100c5c161 _TPA__TTRXFo_oCSo20NSURLSessionDataTaskoGSqPSs9AnyObject___dT__XFo_iTS_GSqPS0_____iT__ + 81
    19  Subproperty Fails                   0x0000000100c5cf77 _TTRXFo_iTCSo20NSURLSessionDataTaskGSqPSs9AnyObject____iT__XFo_oS_oGSqPS0____dT__ + 39
    20  Subproperty Fails                   0x0000000100c5cfcd _TTRXFo_oCSo20NSURLSessionDataTaskoGSqPSs9AnyObject___dT__XFdCb_dS_dGSqPS0____dT__ + 77
    21  AFNetworking                        0x0000000100d1b021 __116-[AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:]_block_invoke97 + 241
    22  AFNetworking                        0x0000000100d3e1db __72-[AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]_block_invoke_2151 + 203
    23  libdispatch.dylib                   0x0000000104782e5d _dispatch_call_block_and_release + 12
    24  libdispatch.dylib                   0x00000001047a349b _dispatch_client_callout + 8
    25  libdispatch.dylib                   0x000000010478b2af _dispatch_main_queue_callback_4CF + 1738
    26  CoreFoundation                      0x0000000101461d09 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    27  CoreFoundation                      0x00000001014232c9 __CFRunLoopRun + 2073
    28  CoreFoundation                      0x0000000101422828 CFRunLoopRunSpecific + 488
    29  GraphicsServices                    0x0000000106a6bad2 GSEventRunModal + 161
    30  UIKit                               0x0000000101d1e610 UIApplicationMain + 171
    31  Subproperty Fails                   0x0000000100c5fb0d main + 109
    32  libdyld.dylib                       0x00000001047d792d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Walking up the backtrace to find the caller of valueForKey we see:

_TFFeRdq_C10RealmSwift6Object_10GeoQueriesCS_7Results15filterGeoRadiusuRdq_S0__FGS2_q__FTVSC22CLLocationCoordinate2D6radiusSd13sortAscendingGSqSb_11latitudeKeySS12longitudeKeySS11distanceKeySS_GSaq__U_FS0_Sb

Demangling that name gives:

ext.GeoQueries.RealmSwift.Results<A where A: RealmSwift.Object>.(filterGeoRadius <A where A: RealmSwift.Object> (RealmSwift.Results<A>) -> (__C.CLLocationCoordinate2D, radius : Swift.Double, sortAscending : Swift.Bool?, latitudeKey : Swift.String, longitudeKey : Swift.String, distanceKey : Swift.String) -> [A]).(closure #1)

So the exception is coming from within filterGeoRadius. Changing the two calls to valueForKey to valueForKeyPath within filterGeoRadius gets me past this exception. It blows up slightly later due to trying to store to a dist property that doesn't appear to exist on any of the model classes. Tweaking the call to findNearby to drop the distanceKey parameter gets around that issue.

mhergon commented 8 years ago

It works! Thank you @bdash