-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Important Idea/Feature: Better Nested Object Support #118
Comments
Hi @CatGuardian, Thanks for the really well documented feature request, it's definitely something that has stood out as a sore point in using Iridium ever since the initial transform framework was added and something I've been wanting to solve for a while (but alas, one never has as much time as they think they do). Let me start by laying out some of the edge cases that creep in when dealing with transforms and then how the current transform framework is implemented, after which I'll touch on your design proposal and we can discuss what feature priorities look like for a redesign of this aspect of Iridium. Requirements
Edge Cases/Gotchas(And a sprinkling of info about the current implementation)
Current ImplementationThe current transform implementation has two distinct components: This is what you're seeing with complex properties, where changing a nested element doesn't trigger the transform. This results in a disconnect between the "disconnected" transformed object (which you made the changes to) and the It can also get worse in situations where the transform does a shallow-copy but does deep-transforms. Let's take the following example: interface Doc {
items: {
name: string
upper?: bool
}[]
}
function toDB(data: Doc) {
return data.map(item => { name: item.upper ? item.name.toUpperCase() : item.name.toLowerCase() })
}
function fromDB(data: Doc) {
return data.map(item => item)
} In this scenario, the following code would result in a instance.document // { items: [{ name: "lowercase" }] }
const item = instance.items[0]
item.upper = true
item.name = "TitleCase"
instance.document // { items: [{ name: "TitleCase", upper: true }] }
// EXPECTED:
instance.document // { items: [{ name: "TITLECASE" }] } So it's obvious this doesn't work particularly intuitively. The solution I've applied is to always tread the values of instance fields as immutable (replace the whole thing or don't change it) - however that obviously isn't enforced, which causes the issues you're seeing. instance.document // { items: [{ name: "lowercase" }] }
instance.items = [{ name: "TitleCase", upper: true }, instance.items.slice(1)...]
instance.document // { items: [{ name: "TITLECASE" }] } Your Design ProposalOkay, so now that I've done a brain dump of the current state of things and some of the issues that need to be solved by the transforms framework, let's talk about your proposal. First of all, thanks for putting in so much effort - it looks great and it's clear you've thought through a number of use cases and problems that one can encounter. To start off, I agree that Iridium needs to do a better job of handling complex fields. They're a really common use case with MongoDB and not handling them nicely is akin to saying "Well, just use Redis". Ideally we'd want to avoid introducing any other edge-cases and gotchas if possible while still solving that problem, but depending on what those gotchas are, they may well be worth it. Your suggestion of having a Unfortunately, it's also pretty monolithic and your Solving that problem I think is the crux of this issue. From an API perspective we'd like to be able to treat complex field modifications the same way we would treat simple field modifications (I make a change to it, it gets transformed and saved). The current blocker on that working as expected is the fact that we only evaluate the field transforms when the field is assigned to, or read from. That works great if the field will always be assigned to when the value is mutated, but that's not the case here. Behind the scenes, we need the field assignment to occur so that this line gets executed and the underlying object gets mutated - however that's an implementation detail and one we can change (read: fix) to ensure it behaves as expected. To achieve that, we could look at leveraging your suggestion of memoization to keep track of a "modified const items = instance.items // `items` is memoized in `instance._memoize["items"]`
items[0].upper = true
items[0].name = "TitleCase"
const newItems = instance.items // returns the memoized `items`
instance.document // When this is read we re-apply all the `instance._memoize` entries before returning it, ensuring it is up to date with the changes
instance.save() // When this is called we re-apply the `instance._memoize` entries as well This approach also has the nice benefit of introducing memoization as a performance improvement for reading those complex properties multiple times, however it would bring with it an added memory cost - so it might be necessary to provide an option to disable it for users where that is of greater concern. What do you think? Can you see any issues that would prevent this from working the way you're expecting, or why it might not behave correctly under certain use cases? |
Hi Benjamin, Wow that was a huge response. First thanks for the time it took to put all that together. I think your 'memoize' solution would work. I hadn't considered what other variables would need to be kept updated as the nested object changes. Like I didn't consider that In your solution then, when a property with a fromDb transform is read (
And when a property with a So now is a good time to mention how I want to use the interface that defines the document. Take the below for example.
Notice how the That was a slight tangent but I wanted to make sure we are using the Back to your memoize idea.
I think I am liking this idea you proposed. Please respond with any comments you have. |
Also I have a question. How does Iridium know which properties of my class it should save to the database? For example:
I think in order for a property to be saved to the db, it has to be defined in the model's schema right? So since I didn't use |
Hi @CatGuardian, you seem to have exactly understood the idea - all your pseudocode is spot on and the use cases you're describing align with what I'm expecting. The only slight change I'd likely make is that after updating As an aside, your idea of using the function ClassTransform<T extends { toDB(): any; }>(cls: typeof T) {
return Iridium.Transform((value: any) => new cls(value), (value: T) => value.toDB())
}
function ClassListTransform<T extends { toDB(): any; }[]>(cls: typeof T) {
return Iridium.Transform((value: any[]) => value.map(v => new cls(v)), (value: T[]) => value.map(v => v.toDB()))
} This would then let you define your model this way: @Index({ name: 1 })
@Collection('houses')
class House extends Instance<HouseDocument, House> implements HouseDocument {
@ObjectID _id: string;
@Property(/^.+$/)
name: string;
@Property(CarSchema)
@ClassListTransform(Car)
cars: Car[];
} As for how Iridium knows which properties to save - anything on As for the next steps, I think this is likely a pretty small implementation so I'll try and set aside time to look at it this week - however I'm afraid (as I'm sure you've noticed) I've been absolutely swamped with other stuff recently and the amount of time I've had to dedicate to Open Source has been almost |
This change moves the logic which determines how a field is access out of ModelSpecificInstance and into Instance instead. This not only allows engineers to provide their own implementations if they wish, but also allows us to start adding additional functionality for it more easily (as part of #118).
…nces This makes it possible to make modifications to objects which are the result of field transforms in a safe manner which is reflected in the changes made to the database. This should enable complex objects to work intuitively through transforms without the need for re-assigning to the field when you are done. Additionally, this fixes an issue with transformed fields when an instance is saved for the first time - by both fixing the bug that caused it and strictly defining the roles of TDocument and TInstance (with TDocument now representing the database-types and TInstance representing the post-transform types).
Hi @CatGuardian, I've just pushed out Thanks |
Hi Benjamin, That is awesome that you went ahead and did this. Thank you a bunch! Here are the things I want to do but I don't have time to do this weekend:
Also as a side note, I would be way more than happy to do pull request reviews on stuff before they are merged in if you wanted someone to look at it before hand. For this particular feature I think that would have been beneficial because I think I spotted something we didn't consider. Which brings me to the real reason I logged in to GitHub today: I don't think we properly considered the Like I mentioned in my task list, I haven't had time to review your changes so it might have been noticed and resolved but I wanted to bring it up to make sure. The problem I am seeing (before your changes) is that the
Since my
But typescript is telling me that Again, this was before your changes and I have not reviewed your changes so I don't know if u fixed this or not. But if you didn't fix it then we have to do 1 of 2 things:
So what are your comments about that? |
I haven't looked at your commits yet but I just wanted to jot done some potential TODO's from this feature. I don't expect you to necessarily do these. I can help too. I just don't want to forget these things.
I might add more to this comment via edit later if I think of more. |
No rush, there's still a bunch more stuff I want to address before 8.x gets released as a As for the behaviour of Traditionally The first step of that has been updating the README with a new set of examples which show this distinction, as well as the release notes for |
So the TL;DR is that you weren't really doing things incorrectly before (since there wasn't a well defined correct/incorrect way to do things) but with the latest version the approach of using |
Ok I agree as I was looking through the source code again for a different bug (#119). And it makes the best sense for TDocument to look like the actual database representation. So I will make sure I do that from now on. Thanks for the responses. I'll post again after I get around to looking at the changes. |
Perfect 👍 |
Hi Benjamin,
I have been doing some thinking lately about how Iridium requires you to reassign a nested object in order to get it to be saved. I have come to the conclusion that the forced reassignment is not desired. I think Iridium should treat nested objects as a normal thing. Right now it kind of feels like nested objects are treated as, for a lack of a better term, 'second class citizens'.
Here is what I'm talking about (snippet taken from Transform section of main docs):
Notice how
the
instance.position
didn't change when you assignedpos.lat = 3?
I believe that modifying
pos.lat
should be reflected the next time I doinstance.save()
.Well this is a slightly odd example because you are doing
let pos = instance.pos;
and modifyingpos.lat
.But if I modify
instance.pos.lat
(which is more of the use case that I see being normal usage) the same thing happens; that is if I modifyinstance.pos.lat
and then callinstance.save()
, then the modification I did still won't be saved.My philosophy is that if I retrieve an object from the database and modify any part of it, then it should be saved back to the database.
A good example is if I have a
User
collection which has alicense
field which is an object with various fields. I want to declare mylicense
field as aLicense
class that I define. So when I get aUser
from the database I want it to do a transform from the database and do what is necessary to create thenew License(...)
and make thelicense
field of myUser
an actualLicense
object. And then I want to douser.license.states.push('Nebraska'); // License has a states property which is an array of strings
followed byuser.save()
and I expect 'Nebraska' to be saved to the database.So I really think Iridium should be updated to support this.
Now I saw that the documentation says:
But I think transforming everything once when the document is retrieved from the database and then once again when the instance is being saved to the database isn't a performance hit. Right now any time a property is accessed that has a fromDb transform, the transform is executed. And any time a property that has a toDb transform is assigned, the transform is executed. So it seems like transforms are executed more often than what I am proposing.
Even if I am wrong, that is even if there is a huge performance hit if you don't defer the transform until something wants it, then there is still a solution to defer execution of the transform. It is possible to implement it so that the transform is only executed once on the first time the property is accessed AND to have it store the result of the transform AND have any future accesses to that property return the stored result. Then when it comes time to save back to the database, the toDb transform is ran once on the stored object and the result of the toDb transform is used as the document to save. However this way is more complicated to implement than my other idea of transforming it once when it comes from the DB and executing the the toDb transform once when it is about to be saved to the database.
I think Iridium should handle nested objects. One idea is to have the Iridium API look for a
toIridium()
or atoDocument()
or atoDbDocument()
method on nested objects and if that is present, call that method when trying to save to the database and use its result as the_modified
document to try saving. Then that allows Classes to be defined that implement that method. So myLicense
class could implement that method and then Iridium would now how to save it. Of course you will probably still want to support thetoDb
transform. So I would think the precedence would go:toDbDocument()
exists on the object use it.toDb
transform exists for that property, then use it.So right now I am curious to know what your thoughts are on this matter. Again I think this is a very important feature that Iridium lacks. Then we can go from there once I hear your thoughts.
The text was updated successfully, but these errors were encountered: