Closed ffelicioautodoc closed 1 year ago
Your code is trying to get the length of a realm list on a realm object that is already dead, ie. deleted in the database.
If you need to access data on an object after it is deleted, consider freezing it before deleting it. That way you can still access the old state.
Otherwise, re-structure your code to avoid accessing deleted realm objects.
Good afternoon @nielsenko
Thank you very much for answering.
Sorry for my lack of intelligence, but when you say:
Your code is trying to get the length of a realm list on a realm object that is already dead, ie. deleted in the database.
If you need to access data on an object after it is deleted, consider freezing it before deleting it. That way you can still access the old state.
Otherwise, re-structure your code to avoid accessing deleted realm objects.
What would that be?
Do you have any examples to point out?
What left us confused is that, we do this same process in the other objects of the project and the problem is not generated, only in this resource that the error is sent to us.
import 'package:realm_dart/realm.dart'; // using plain dart instead of flutter here, but concept is the same
part 'example.g.dart'; // assuming this file is example.dart
@RealmModel()
class _Stuff {
late int id;
}
final realm = Realm(Configuration.local([Stuff.schema]));
void main(List<String> arguments) {
final x = realm.write(() => realm.add(Stuff(1)));
final y = x.freeze();
realm.write(() => realm.delete(x));
print(y.id); // <-- safe since y was frozen (ie. point to old version of x)
print(x.id); // <-- throws since x is deleted from realm.
}
outputs:
1
Unhandled exception:
RealmException: Error getting property Stuff.id Error: RealmException: Error code: 7 . Message: Accessing object of type Stuff which has been invalidated or deleted
#0 RealmCoreAccessor.get (package:realm_dart/src/realm_object.dart:213:7)
#1 RealmObjectBase.get (package:realm_dart/src/realm_object.dart:316:29)
...
In package:rdo/presentation/bootstrap.dart:13
you have hooked up some RDOBlocObserver.onChange
that eventually ends up accessing the length of a realm list on a dead realm object while constructing an iterator.
You didn't show me that code, but that is where the stack trace leaves your code, as far as I can tell.
BTW, in:
final activitiesModelList = activitiesChange.changes.map((event) => event
.results
.toList() // <-- this hydrates the full list
.map((activity) => ActivitiesModel.fromRealm(activity)) // <-- this hydrates every object
.toList());
you are loosing a lot of the benefits of realm in my opinion.
After that you have a stream of fully instantiated List<ActivitiesModel>
s. Nothing is loaded lazy after here, if I guess the implementation of ActivitiesModel.fromRealm
correctly.
If your lists are small (and since realm is fast) it will work, but consider constructing bloc/view model like objects lazily, and don't copy out properties from your realm objects, but access them on demand.
In the short term you could consider guarding against accessing dead realm objects by sprinkling strategic if (x.isValid)
(where x
is a realm object) throughout your code, but it becomes messy fast.
...
print(y.id); // <-- safe since y was frozen (ie. point to old version of x)
if (!x.isValid) print('x is dead'); // <-- safe to call isValid on dead objects
print(x.id); // <-- throws since x is deleted from realm.
...
In
package:rdo/presentation/bootstrap.dart:13
you have hooked up someRDOBlocObserver.onChange
that eventually ends up accessing the length of a realm list on a dead realm object while constructing an iterator.You didn't show me that code, but that is where the stack trace leaves your code, as far as I can tell.
Good Morning @nielsenko!
I'm sorry for the delay in responding.
So this resource is just a bloc observer that helps us map the flows. We are currently enjoying the benefits of the stream that the realm lib gives us together with the features of flutter_bloc.
class RDOBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
log('onChange(${bloc.runtimeType}, $change)'); // <- this is line 13 in the code
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
log('onError(${bloc.runtimeType}, $error, $stackTrace)');
super.onError(bloc, error, stackTrace);
}
}
BTW, in:
final activitiesModelList = activitiesChange.changes.map((event) => event .results .toList() // <-- this hydrates the full list .map((activity) => ActivitiesModel.fromRealm(activity)) // <-- this hydrates every object .toList());
you are loosing a lot of the benefits of realm in my opinion.
After that you have a stream of fully instantiated
List<ActivitiesModel>
s. Nothing is loaded lazy after here, if I guess the implementation ofActivitiesModel.fromRealm
correctly.If your lists are small (and since realm is fast) it will work, but consider constructing bloc/view model like objects lazily, and don't copy out properties from your realm objects, but access them on demand.
So in this case, would it be better to use the model classes that Realm provides us?
This class you showed is a mapper that transforms the Realm class into a class to be used in our domain.
I'll leave below the implementation of this class that you indicated:
class ActivitiesModel extends ActivitiesEntity with EquatableMixin {
final String? idModel;
final String rdoIdModel;
final String? statusModel;
final List<String>? placeModel;
final String? descriptionModel;
final AuditModel? createdAtModel;
final AuditModel? updatedAtModel;
ActivitiesModel({
this.idModel,
required this.rdoIdModel,
this.statusModel,
this.placeModel,
this.descriptionModel,
this.createdAtModel,
this.updatedAtModel,
}) : super(
id: idModel,
rdoId: rdoIdModel,
status: statusModel,
place: placeModel,
description: descriptionModel,
createdAt: createdAtModel,
updatedAt: updatedAtModel,
);
// mapper that turns the Realm object into a managed class in the project
factory ActivitiesModel.fromRealm(Activities activities) {
return ActivitiesModel(
idModel: activities.id.hexString,
rdoIdModel: activities.rdoId.hexString,
statusModel: activities.status,
placeModel: activities.place,
descriptionModel: activities.description,
createdAtModel: activities.createdAt != null
? AuditModel.fromRealm(activities.createdAt!)
: null,
updatedAtModel: activities.updatedAt != null
? AuditModel.fromRealm(activities.updatedAt!)
: null,
);
}
// mapper that transforms the domain class to a Realm object
// It is used for create/update/delete operations
Activities toRealm() {
return Activities(
ObjectId.fromHexString(idModel ?? ObjectId().hexString),
ObjectId.fromHexString(rdoIdModel),
status: statusModel,
place: placeModel ?? [],
description: descriptionModel,
createdAt: createdAtModel?.toRealm(),
updatedAt: updatedAtModel?.toRealm(),
);
}
factory ActivitiesModel.fromEntity(ActivitiesEntity activitiesEntity) {
return ActivitiesModel(
idModel: activitiesEntity.id,
rdoIdModel: activitiesEntity.rdoId,
statusModel: activitiesEntity.status,
placeModel: activitiesEntity.place,
descriptionModel: activitiesEntity.description,
createdAtModel: activitiesEntity.createdAt == null
? null
: AuditModel.fromEntity(activitiesEntity.createdAt!),
updatedAtModel: activitiesEntity.updatedAt == null
? null
: AuditModel.fromEntity(activitiesEntity.updatedAt!),
);
}
ActivitiesModel copyWith({
String? id,
String? rdoId,
String? period,
String? weather,
String? condition,
String? description,
List<String>? place,
AuditModel? createdAt,
AuditModel? updatedAt,
}) {
return ActivitiesModel(
idModel: id ?? idModel,
rdoIdModel: rdoId ?? rdoIdModel,
statusModel: status ?? statusModel,
placeModel: place ?? placeModel,
descriptionModel: description ?? descriptionModel,
createdAtModel: createdAt ?? createdAtModel,
updatedAtModel: updatedAt ?? updatedAtModel,
);
}
@override
List<Object?> get props => [
idModel,
rdoIdModel,
statusModel,
placeModel,
descriptionModel,
createdAtModel,
updatedAtModel,
];
}
Regarding the line 13 in RDOBlocObserver
and looking at the stacktrace:
...
#30 Change.toString
package:bloc/src/change.dart:31
#31 _StringBase._interpolate (dart:core-patch/string_patch.dart:851:19)
#32 RDOBlocObserver.onChange
...
It is the $change
that evokes a Change.toString()
that tries to describe before and after. But here the realm list is already dead, so that fails.
Regarding ActivitiesModel.fromRealm
it is as I suspected. Note how you are accessing every property of the realm object in the factory constructor, and copying the values out into a new ActivitiesModel
object. This means you have defeated the lazy loading that happens when accessing the properties of a realm object.
It is not that it won't work, but it is working a bit against the grain of realm, I think.
I hope you got a few pointers to work on. Good luck with your project @ffelicioautodoc!
Regarding the line 13 in
RDOBlocObserver
and looking at the stacktrace:... #30 Change.toString package:bloc/src/change.dart:31 #31 _StringBase._interpolate (dart:core-patch/string_patch.dart:851:19) #32 RDOBlocObserver.onChange ...
It is the
$change
that evokes aChange.toString()
that tries to describe before and after. But here the realm list is already dead, so that fails.Regarding
ActivitiesModel.fromRealm
it is as I suspected. Note how you are accessing every property of the realm object in the factory constructor, and copying the values out into a newActivitiesModel
object. This means you have defeated the lazy loading that happens when accessing the properties of a realm object.It is not that it won't work, but it is working a bit against the grain of realm, I think.
I hope you got a few pointers to work on. Good luck with your project @ffelicioautodoc!
Thanks a lot for your support @nielsenko.
As you yourself described, code review actions were carried out in order to find the problem.
I will review with the team about the possibility of directly using classes generated by Realm. As you explained, we are losing the benefits that the library gives us.
As we use the design following the Clean Architecture pattern, we don't think about the resources delivered by Realm objects. We ended up following the pattern as if it were using an api.
In this case, would it be better for our entire project to only use Realm classes (without using mappers/converters)? Was that really what you meant?
I apologize for the problem created and again, thank you for your support and your team in helping with this task.
In this case, would it be better for our entire project to only use Realm classes (without using mappers/converters)? Was that really what you meant?
Well, don't introduce mappers just because that is how you would normally do with a database. There are valid reasons to have wrappers, but use them sparsely and try to construct them lazily when you do, ie. when accessing index i
of a list, instead of copying the entire list eagerly.
I don't know you project in detail, so it would be a bit presumptuous of me to say don't use mappers, but at least be careful when using them together with Realm.
I would like to continue this conversation with mappers, because I stumbled with same problem. I noticed especially when I have large list of data objects that are mapped, there application loses performance hard, reasons you stated above -> objects are not load lazily but every value is read and converter.
I have just no idea how to handle this problem correctly, can you show code example, what do you mean by "when accessing index i of a list, instead of copying the entire list eagerly.". I need app objects from realm for several reasons:
I wanted to find maybe there are ways how I can map all list to from realm objects to app objects lazily, so that I didn't lose lazy realm object benefits, but again I don't know if and how it's possible. Also I couldn't understand in app model how to handle multiple relationship lazy loading, so I don't load large chunks of data, if I load and map all objects that have multiple relationships.
I would be really thankful if you could provide solution of realm best practice to handle this problem.
I have just noticed .toList() is hydrating the full list but Iterable seems like not, meaning toList() affects performance hard, compared to leaving it just as Iterable, where performance is just slightly affected.
I have also noticed if realm object has relationship to multiple objects late List<_Person> drivers;
, for some reason local.drivers.map((driver) => toPersonApp(driver)).toList()
, doesn't lose any performance here, only lose performance if mapping RealmResults to List.
@ebelevics Yes, when calling toList
on any implementor of List
(including RealmList
) you are copying all elements and creating a new dart List
. This defeats all the nice lazy speed and space benefits of RealmList
, and you also loose the ability to listen for changes.
It is a bit unclear what you are trying to say explain here:
I have also noticed if realm object has relationship to multiple objects late List<_Person> drivers;, for some reason
local.drivers.map((driver) => toPersonApp(driver)).toList()
, doesn't lose any performance here, only lose performance if mapping RealmResults to List.
In particular since I can only guess about toPersonApp
. But perhaps you are asking why there is no difference in performance between:
for (final x in local.drivers.map((driver) => toPersonApp(driver)).toList()) {
// do something ..
}
and
for (final d in local.drivers) {
final x = toPersonApp(d);
// do something ..
}
If so, then yes, there is no difference - the work is the same. You are iterating over the full list hydrating and mapping every thing in both cases.
But there is a (potentially big - depending on size of the list) difference between doing:
toPersonApp(local.drivers[424242]); // fetch handle to a single Person, then hydrates and maps it.
and
local.drivers.map((driver) => toPersonApp(driver)).toList()[424242]; // hydrates and maps every Person, then select a single.
First is O(1)
second is O(n)
where n
is the size of local.drivers
. It obviously depends on the size of lists if this is actually something your users will feel.
Now to your original question..
Regarding non-realm types such as Color
you need to do the mapping, fx:
@RealmModel()
class _Pen {
@MapTo('color')
late int colorAsInt;
Color get color => Color(colorAsInt);
set color(Color c) => colorAsInt = c.value;
}
I hope we can make this more convenient later.
The non-nullable relations is impossible for us in general, since we cannot prevent some part of your system from deleting the objects pointed to. But if you can guarantee that won't happen, you can do something similar as with Color
above, and stripping the nullability:
@RealmModel()
class _Line {
@MapTo('pen')
late _Pen? penBacking;
_Pen get pen => penBacking!;
set pen(_Pen p) => penBacking = p;
}
If it is worth the anxiety it to prevent !.
anxiety .. maybe, maybe not.
Regarding, separation of database layer, and clean architecture.. Introducing abstractions to easily replace core components in your app is a bit overused in my opinion. You wouldn't abstract away the fact that your are using flutter, just in case a different UI framework come along for Dart at one point. More to the point, the common abstraction layer we all learned to hide the database behind does not play well with realm, as you are starting to realise. In general I suggest you model your entities directly as realm objects, and don't don't treat realm objects as just something to traffic data in and out of the database. This jives better with Realm.
I hope this helps a bit..
Anyway.. if you still prefer to map entities and results, and cannot live with the performance of all the copying you have introduced, then you need to start looking a writing lazy list wrappers. Here is a simple one:
class LazyMappedList<From, To> with ListMixin<To> {
final List<From> _source;
final To Function(From) _mapper;
final From Function(To)? _reverseMapper;
LazyMappedList(this._source, this._mapper, [this._reverseMapper]);
@override
int get length => _source.length;
@override
set length(int newLength) => _source.length = newLength; // this is likely to cause trouble..
@override
To operator [](int index) {
return _mapper(_source[index]);
}
@override
void operator []=(int index, To value) {
if (_reverseMapper == null) {
throw UnsupportedError('Cannot set value of a lazy mapped list');
}
_source[index] = _reverseMapper!(value);
}
}
This is of the top of my head, haven't tested it at all..
BTW, it is better (for me/us at least) to open a new issue, instead of piggy-backing on an old one. Even if the question is closely related. You can always refer to the previous issue from the new one.
What happened?
When executing the deletion process of a record (via the Atlas platform or the process on the device), the message described is returned.
Here we have updated to realm version: ^0.10.0+rc, but the error occurs since version 8
As you can see below, the records exist in the Atlas console, in Realm Studio and are being presented in the interface.
To interconnect the interface with the data, we are using the flutter_bloc lib.
[ATLAS - CONSOLE]
[REALM STUDIO]
[UI] UNITO-UNDERSCORE!1675105757!
Realm Flutter Version: realm: ^0.10.0+rc
Repro steps
When deleting a record via the atlas platform or any action taken on the device, the following error is generated:
[GENERATED ERROR]
Version
Flutter Version: Flutter 3.7.0 / Dart: Dart 2.19.0
What Atlas Services are you using?
Atlas Device Sync
What type of application is this?
Flutter Application
Client OS and version
Android 8 / Android 10 / Android 13 (emulator) / Iphone 12 / Iphone 14
Code snippets
Stacktrace of the exception/crash you're getting
Relevant log output