Skip to content

Commit 364f546

Browse files
authored
Add custom implementations for toString, equals and hashCode (#1480)
1 parent 310556e commit 364f546

File tree

17 files changed

+800
-115
lines changed

17 files changed

+800
-115
lines changed

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
## 1.11.0-SNAPSHOT (YYYY-MM-DD)
22

33
### Breaking Changes
4-
* None.
4+
* `BaseRealmObject.equals()` has changed from being identity-based only (===) to instead return `true` if two objects come from the same Realm version. This e.g means that reading the same object property twice will now be identical. Note, two Realm objects, even with identical values will not be considered equal if they belong to different versions.
5+
6+
```
7+
val childA: Child = realm.query<Child>().first().find()!!
8+
val childB: Child = realm.query<Child>().first().find()!!
9+
10+
// This behavior is the same both before 1.11.0 and before
11+
childA === childB // false
12+
13+
// This will return true in 1.11.0 and onwards. Before it will return false
14+
childA == childB
15+
16+
realm.writeBlocking { /* Do a write */ }
17+
val childC = realm.query<Child>().first().find()!!
18+
19+
// This will return false because childA belong to version 1, while childC belong to version 2.
20+
// Override equals/hashCode if value semantics are wanted.
21+
childA == childC
22+
```
523

624
### Enhancements
25+
* Realm model classes now generate custom `toString`, `equals` and `hashCode` implementations. This makes it possible to compare by object reference across multiple collections. Note that two objects at different versions will not be considered equal, even
26+
if the content is the same. Custom implementations of these methods will be respected if they are present. (Issue [#1097](https://github.com/realm/realm-kotlin/issues/1097))
727
* Support for performing geospatial queries using the new classes: `GeoPoint`, `GeoCircle`, `GeoBox`, and `GeoPolygon`. See `GeoPoint` documentation on how to persist locations. (Issue [#1403](https://github.com/realm/realm-kotlin/pull/1403))
828
* Support for automatic resolution of embedded object constraints during migration through `RealmConfiguration.Builder.migration(migration: AutomaticSchemaMigration, resolveEmbeddedObjectConstraints: Boolean)`. (Issue [#1464](https://github.com/realm/realm-kotlin/issues/1464)
929
* [Sync] Add support for customizing authorization headers and adding additional custom headers to all Atlas App service requests with `AppConfiguration.Builder.authorizationHeaderName()` and `AppConfiguration.Builder.addCustomRequestHeader(...)`. (Issue [#1453](https://github.com/realm/realm-kotlin/pull/1453))

packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectHelper.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@
1717
package io.realm.kotlin.internal
1818

1919
import io.realm.kotlin.UpdatePolicy
20+
import io.realm.kotlin.VersionId
2021
import io.realm.kotlin.dynamic.DynamicMutableRealmObject
2122
import io.realm.kotlin.dynamic.DynamicRealmObject
2223
import io.realm.kotlin.ext.asRealmObject
24+
import io.realm.kotlin.ext.isManaged
25+
import io.realm.kotlin.ext.isValid
2326
import io.realm.kotlin.ext.toRealmDictionary
2427
import io.realm.kotlin.ext.toRealmList
2528
import io.realm.kotlin.ext.toRealmSet
2629
import io.realm.kotlin.internal.dynamic.DynamicUnmanagedRealmObject
2730
import io.realm.kotlin.internal.interop.ClassKey
2831
import io.realm.kotlin.internal.interop.CollectionType
2932
import io.realm.kotlin.internal.interop.MemAllocator
33+
import io.realm.kotlin.internal.interop.ObjectKey
3034
import io.realm.kotlin.internal.interop.PropertyKey
3135
import io.realm.kotlin.internal.interop.PropertyType
3236
import io.realm.kotlin.internal.interop.RealmInterop
@@ -39,6 +43,7 @@ import io.realm.kotlin.internal.interop.RealmValue
3943
import io.realm.kotlin.internal.interop.Timestamp
4044
import io.realm.kotlin.internal.interop.getterScope
4145
import io.realm.kotlin.internal.interop.inputScope
46+
import io.realm.kotlin.internal.platform.identityHashCode
4247
import io.realm.kotlin.internal.platform.realmObjectCompanionOrThrow
4348
import io.realm.kotlin.internal.schema.ClassMetadata
4449
import io.realm.kotlin.internal.schema.PropertyMetadata
@@ -1145,6 +1150,66 @@ internal object RealmObjectHelper {
11451150
}
11461151
}
11471152

1153+
@Suppress("unused") // Called from generated code
1154+
// Inlining this functions somehow break the IntelliJ debugger, unclear why?
1155+
internal fun realmToString(obj: BaseRealmObject): String {
1156+
// This code assumes no race conditions
1157+
val schemaName = obj::class.realmObjectCompanionOrNull()?.io_realm_kotlin_className
1158+
val fqName = obj::class.qualifiedName
1159+
return obj.realmObjectReference?.let {
1160+
if (obj.isValid()) {
1161+
val id: RealmObjectIdentifier = obj.getIdentifier()
1162+
val objKey = id.objectKey.key
1163+
val version = id.versionId.version
1164+
"$fqName{state=VALID, schemaName=$schemaName, objKey=$objKey, version=$version, realm=${it.owner.owner.configuration.name}}"
1165+
} else {
1166+
val state = if (it.owner.isClosed()) {
1167+
"CLOSED"
1168+
} else {
1169+
"INVALID"
1170+
}
1171+
"$fqName{state=$state, schemaName=$schemaName, realm=${it.owner.owner.configuration.name}, hashCode=${obj.hashCode()}}"
1172+
}
1173+
} ?: "$fqName{state=UNMANAGED, schemaName=$schemaName, hashCode=${obj.hashCode()}}"
1174+
}
1175+
1176+
@Suppress("unused", "ReturnCount") // Called from generated code
1177+
// Inlining this functions somehow break the IntelliJ debugger, unclear why?
1178+
internal fun realmEquals(obj: BaseRealmObject, other: Any?): Boolean {
1179+
if (obj === other) return true
1180+
if (other == null || obj::class != other::class) return false
1181+
1182+
other as BaseRealmObject
1183+
1184+
if (other.isManaged()) {
1185+
if (obj.isValid() != other.isValid()) return false
1186+
return obj.getIdentifierOrNull() == other.getIdentifierOrNull()
1187+
} else {
1188+
// If one of the objects are unmanaged, they are only equal if identical, which
1189+
// should have been caught at the top of this function.
1190+
return false
1191+
}
1192+
}
1193+
1194+
@Suppress("unused", "MagicNumber") // Called from generated code
1195+
// Inlining this functions somehow break the IntelliJ debugger, unclear why?
1196+
internal fun realmHashCode(obj: BaseRealmObject): Int {
1197+
// This code assumes no race conditions
1198+
return obj.realmObjectReference?.let {
1199+
val isValid: Boolean = obj.isValid()
1200+
val identifier: RealmObjectIdentifier = if (it.isClosed()) {
1201+
RealmObjectIdentifier(ClassKey(-1), ObjectKey(-1), VersionId(0), "")
1202+
} else {
1203+
obj.getIdentifier()
1204+
}
1205+
val realmPath: String = it.owner.owner.configuration.path
1206+
var hashCode = isValid.hashCode()
1207+
hashCode = 31 * hashCode + identifier.hashCode()
1208+
hashCode = 31 * hashCode + realmPath.hashCode()
1209+
hashCode
1210+
} ?: identityHashCode(obj)
1211+
}
1212+
11481213
private fun checkPropertyType(
11491214
obj: RealmObjectReference<out BaseRealmObject>,
11501215
propertyName: String,

packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmObjectUtil.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,15 @@ internal fun BaseRealmObject.getIdentifier(): RealmObjectIdentifier {
142142
val classKey: ClassKey = metadata.classKey
143143
val objKey: ObjectKey = RealmInterop.realm_object_get_key(objectPointer)
144144
val version: VersionId = version()
145-
return Triple(classKey, objKey, version)
145+
val path: String = owner.owner.configuration.path
146+
return RealmObjectIdentifier(classKey, objKey, version, path)
146147
} ?: throw IllegalStateException("Identifier can only be calculated for managed objects.")
147-
ULong
148+
}
149+
150+
public fun BaseRealmObject.getIdentifierOrNull(): RealmObjectIdentifier? {
151+
return runIfManaged {
152+
getIdentifier()
153+
}
148154
}
149155

150156
/**

packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmUtils.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,15 @@ import kotlin.reflect.KProperty1
4848
// `equals` method, which in general just is the memory address of the object.
4949
internal typealias UnmanagedToManagedObjectCache = MutableMap<BaseRealmObject, BaseRealmObject> // Map<OriginalUnmanagedObject, CachedManagedObject>
5050

51-
// For managed realm objects we use `<ClassKey, ObjectKey, Version>` as a unique identifier
51+
// For managed realm objects we use `<ClassKey, ObjectKey, Version, Path>` as a unique identifier
5252
// We are using a hash on the Kotlin side so we can use a HashMap for O(1) lookup rather than
5353
// having to do O(n) filter with a JNI call for `realm_equals` for each element.
54-
internal typealias RealmObjectIdentifier = Triple<ClassKey, ObjectKey, VersionId>
54+
public data class RealmObjectIdentifier(
55+
val classKey: ClassKey,
56+
val objectKey: ObjectKey,
57+
val versionId: VersionId,
58+
val path: String
59+
)
5560
internal typealias ManagedToUnmanagedObjectCache = MutableMap<RealmObjectIdentifier, BaseRealmObject>
5661

5762
/**

packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,8 @@ public expect fun <K : Any?, V : Any?> returnType(field: KMutableProperty1<K, V>
145145
* Returns whether or not we are running on Windows
146146
*/
147147
public expect fun isWindows(): Boolean
148+
149+
/**
150+
* Returns the identity hashcode for a given object.
151+
*/
152+
internal expect fun identityHashCode(obj: Any?): Int

packages/library-base/src/jvm/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,5 @@ private fun preparePath(directoryPath: String) {
124124
}
125125

126126
public actual fun isWindows(): Boolean = OS_NAME.contains("windows", ignoreCase = true)
127+
128+
internal actual fun identityHashCode(obj: Any?): Int = System.identityHashCode(obj)

packages/library-base/src/nativeDarwin/kotlin/io/realm/kotlin/internal/platform/SystemUtils.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import platform.Foundation.dataWithContentsOfFile
2929
import platform.Foundation.timeIntervalSince1970
3030
import platform.posix.memcpy
3131
import platform.posix.pthread_threadid_np
32+
import kotlin.native.identityHashCode
3233
import kotlin.reflect.KMutableProperty1
3334
import kotlin.reflect.KType
3435

@@ -192,3 +193,5 @@ private fun NSData.toByteArray(): ByteArray = ByteArray([email protected].
192193
}
193194

194195
public actual fun isWindows(): Boolean = false
196+
197+
internal actual fun identityHashCode(obj: Any?): Int = obj.identityHashCode()
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package io.realm.kotlin.compiler
2+
3+
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
4+
import org.jetbrains.kotlin.ir.builders.irGet
5+
import org.jetbrains.kotlin.ir.builders.irGetObject
6+
import org.jetbrains.kotlin.ir.builders.irReturn
7+
import org.jetbrains.kotlin.ir.declarations.IrClass
8+
import org.jetbrains.kotlin.ir.declarations.IrProperty
9+
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
10+
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
11+
import org.jetbrains.kotlin.ir.types.IrType
12+
import org.jetbrains.kotlin.ir.util.functions
13+
import org.jetbrains.kotlin.name.Name
14+
15+
/**
16+
* Class responsible for adding Realm specific logic to the default object methods like:
17+
* - toString()
18+
* - hashCode()
19+
* - equals()
20+
*
21+
* WARNING: The current logic in here does not work well with incremental compilation. The reason
22+
* is that we check if these methods are "empty" before filling them out, and during incremental
23+
* compilation they already have content, and since all of these methods are using inlined
24+
* methods they will not pick up changes in the RealmObjectHelper.
25+
*
26+
* This should only impact us as SDK developers though, but it does mean that changes to
27+
* RealmObjectHelper methods will require a clean build to take effect.
28+
*/
29+
class RealmModelDefaultMethodGeneration(private val pluginContext: IrPluginContext) {
30+
31+
private val realmObjectHelper: IrClass = pluginContext.lookupClassOrThrow(FqNames.REALM_OBJECT_HELPER)
32+
private val realmToString: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmToString"))
33+
private val realmEquals: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmEquals"))
34+
private val realmHashCode: IrSimpleFunction = realmObjectHelper.lookupFunction(Name.identifier("realmHashCode"))
35+
private lateinit var objectReferenceProperty: IrProperty
36+
private lateinit var objectReferenceType: IrType
37+
38+
fun addDefaultMethods(irClass: IrClass) {
39+
objectReferenceProperty = irClass.lookupProperty(Names.OBJECT_REFERENCE)
40+
objectReferenceType = objectReferenceProperty.backingField!!.type
41+
42+
if (syntheticMethodExists(irClass, "toString")) {
43+
addToStringMethodBody(irClass)
44+
}
45+
if (syntheticMethodExists(irClass, "hashCode")) {
46+
addHashCodeMethod(irClass)
47+
}
48+
if (syntheticMethodExists(irClass, "equals")) {
49+
addEqualsMethod(irClass)
50+
}
51+
}
52+
53+
/**
54+
* Checks if a synthetic method exists in the given class. Methods in super classes
55+
* are ignored, only methods actually declared in the class will return `true`.
56+
*
57+
* These methods are created by an earlier step by the Realm compiler plugin and are
58+
* recognized by not being fake and having an empty body.ß
59+
*/
60+
private fun syntheticMethodExists(irClass: IrClass, methodName: String): Boolean {
61+
return irClass.functions.firstOrNull {
62+
!it.isFakeOverride && it.body == null && it.name == Name.identifier(methodName)
63+
} != null
64+
}
65+
66+
private fun addEqualsMethod(irClass: IrClass) {
67+
val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "equals" }
68+
function.body = pluginContext.blockBody(function.symbol) {
69+
+irReturn(
70+
IrCallImpl(
71+
startOffset = startOffset,
72+
endOffset = endOffset,
73+
type = pluginContext.irBuiltIns.booleanType,
74+
symbol = realmEquals.symbol,
75+
typeArgumentsCount = 0,
76+
valueArgumentsCount = 2
77+
).apply {
78+
dispatchReceiver = irGetObject(realmObjectHelper.symbol)
79+
putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol))
80+
putValueArgument(1, irGet(function.valueParameters[0].type, function.valueParameters[0].symbol))
81+
}
82+
)
83+
}
84+
}
85+
86+
private fun addHashCodeMethod(irClass: IrClass) {
87+
val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "hashCode" }
88+
function.body = pluginContext.blockBody(function.symbol) {
89+
+irReturn(
90+
IrCallImpl(
91+
startOffset = startOffset,
92+
endOffset = endOffset,
93+
type = pluginContext.irBuiltIns.intType,
94+
symbol = realmHashCode.symbol,
95+
typeArgumentsCount = 0,
96+
valueArgumentsCount = 1
97+
).apply {
98+
dispatchReceiver = irGetObject(realmObjectHelper.symbol)
99+
putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol))
100+
}
101+
)
102+
}
103+
}
104+
105+
private fun addToStringMethodBody(irClass: IrClass) {
106+
val function: IrSimpleFunction = irClass.symbol.owner.functions.single { it.name.toString() == "toString" }
107+
function.body = pluginContext.blockBody(function.symbol) {
108+
+irReturn(
109+
IrCallImpl(
110+
startOffset = startOffset,
111+
endOffset = endOffset,
112+
type = pluginContext.irBuiltIns.stringType,
113+
symbol = realmToString.symbol,
114+
typeArgumentsCount = 0,
115+
valueArgumentsCount = 1
116+
).apply {
117+
dispatchReceiver = irGetObject(realmObjectHelper.symbol)
118+
putValueArgument(0, irGet(function.dispatchReceiverParameter!!.type, function.dispatchReceiverParameter!!.symbol))
119+
}
120+
)
121+
}
122+
}
123+
}

packages/plugin-compiler/src/main/kotlin/io/realm/kotlin/compiler/RealmModelLoweringExtension.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ private class RealmModelLowering(private val pluginContext: IrPluginContext) : C
107107
// Modify properties accessor to generate custom getter/setter
108108
AccessorModifierIrGeneration(pluginContext).modifyPropertiesAndCollectSchema(irClass)
109109

110+
// Add custom toString, equals and hashCode methods
111+
val methodGenerator = RealmModelDefaultMethodGeneration(pluginContext)
112+
methodGenerator.addDefaultMethods(irClass)
113+
110114
// Add body for synthetic companion methods
111115
val companion = irClass.companionObject() ?: fatalError("RealmObject without companion: ${irClass.kotlinFqName}")
112116
generator.addCompanionFields(irClass, companion, SchemaCollector.properties[irClass])

0 commit comments

Comments
 (0)