-
Notifications
You must be signed in to change notification settings - Fork 362
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
Improve instance-level operation hooks #488
Conversation
The next step would be some 'canned' before hooks which will do the following:
Perhaps we can even allow this syntax: And: This would be the basis for #474 (comment) |
@bajtos @raymondfeng @ritch please review |
I prefer to keep this consistent with "before save" and use
At one hand, nobody should be really expecting that changes to In the light of the above, I prefer to consistently use |
OK, I can live with that. Any other thoughts? If not, I'll apply the changes requested here. |
if (dataSource.connector.findOrCreate) { | ||
observedContexts.should.eql(aTestModelCtx({ | ||
where: { id: existingInstance.id }, | ||
data: { id: existingInstance.id, name: 'updated name' } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo, you are executing updateOrCreate
but testing for findOrCreate
.
Atomic findOrCreate does not set ctx.currentInstance
- we need to capture this in the documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a typo indeed. Sorry. Whenever an optimized (dataSource.connector.findOrCreate) call can be made, the currentInstance
will not be available. However, in the normal, unoptimized scenario, updateAttributes
is invoked internally, which does set currentInstance
. This is why I had to make a distinction here.
See the comment above. Other than the comments above, the implementation looks good. I'd like to hear from @ritch or @raymondfeng whether the name My internet connection is poor in the evenings so I won't be able to continue this discussion until tomorrow. |
@bajtos hope we can merge first thing in the morning. I won't be able to do much tonight, and I'm afraid I'll need a full day to port my code over ... |
Frankly, expecting non-trivial pull requests to get landed the same day is unreasonable, I even dare to say silly. Please expect at least 2-3 working days. |
@bajtos meanwhile I'll fiddle with my thumbs, OK? I have this hunch that nothing ever is really trivial in your opinion ... thanks anyway 👎 |
I'm not worried about the name... my concern is we are adding yet another case you have to handle when using a hook. What would alleviate my concern is if you can use the same assumptions (eg. Also... Why do we need Model.observe('before delete', fetchInstances); // OR:
Model.observe('before delete', fetchIds); or is it only in the case of |
@ritch I tend to agree with you. Initially I only had Ideally, we would just forget about the Finally, for consistency, If we can agree on such a solution, I'd be happy to work on it in the morning. I really need to know which direction we're heading with this - I'm stuck refactoring a huge code-base to be operation hooks compatible - and this Friday is an important deadline for it. @raymondfeng thoughts? |
For the
The main concern for 3 is back-compatibility since |
@raymondfeng if I gather correctly, this is similar to #483 (comment) If an instance is passed in the |
However, working with When available, use Otherwise, accept the limitations of the low-level |
@fabien Do you agree on the following?
|
Let me cross-post my earlier comments explaining why I think it's bad idea to send data & instance as proposed by @raymondfeng.
This is what I disagree with. One of the ideas behind ORM and OOP in general is that the objects should encapsulate the behaviour and hide implementation details. An example is a property with custom setter that creates a normalised version of the input value, or a prototype method that performs some calculation and updates several properties. Another example is a code that changes some model properties and then would like to get a value computed from these changed properties. That's why I believe users should use the instance object to manipulate the data in as many places as possible. Because if they had only the data object, then they would have to build a new instance in order to use these prototype methods. Also because we perform a validation of the updated model instance, in many cases we would have to 1) extract data from the instance before calling the hooks, 2) pass the data back to the instance for validation. At the moment, we can skip 1) because hooks are manipulating the instance directly. My concern is that converting the instance to data is not free, it takes some time and allocates extra memory. I don't want all users to pay this price. Also I believe the hook implementations should have the information whether a partial or a full update will be performed, because the partial update may need special handling. E.g. when applying partial update of "firstName" only on a model with "firstName", "lastName" and computed "fullName", then the hook has to do one of:
@fabien I would really appreciate if you can keep the work in the same pull request the next time, so that the conversation stays in single place.
See the second part of #483 (comment)
There are two basic ways how the hook may want to use the instance: A) It wants to modify the data that will be persisted to the database, this is specific to "before save" hook. We must make it very clear whether the save will update the whole record ( B) It wants to get more information about the model instance affected. IIRC, there are only two cases where we are not exposing this data even though we have it available: Let's focus at When it comes to My conclusion is that we need to come up with a new property name that will be set in the where+data case and that will make it very clear that the changes made to this instance will be ignored by the operation. Perhaps |
One more thought: while the current design of operation hooks may be perceived as too complex, it reflect the complexity of the DAO methods provided by LoopBack and forces users to think more about the different scenarios in which the hook may be invoked. I am concerned that if we simplify the API too much, it may make it too easy to shoot oneself in the foot. |
I think there are some confusions about the terms we use. Let me first clarify what I use:
|
This will break the contract that Other than that, I think this may be a reasonable compromise. I'd rather use a different name for Should the context include both |
As I see it there are two ways to provide a consistent API - we'd have to choose one:
|
@raymondfeng IIUC, if we go with your proposal, then In other words, the following is correct? var isFullUpdate = ctx.data instanceof ctx.Model; |
Yes. |
After giving this more thought, I think option 1 (and thus #488 (comment)) works most consistently. Principle Of Least Surprise, you know ... |
What about the following?
|
As far as I understand the current implementation, the changes are applied to the instance so that the instance can be validated. Which I find a bit weird, because we can't guarantee that the data we see in LoopBack will be the same that will end up in the database, since updateAttributes is making only a partial update. Perhaps if we have Another benefit of |
Note that |
@raymondfeng I personally like |
@raymondfeng @bajtos I propose to land this PR and then refactor at a later stage - it would be a bit too much to expect those who ported their code over to the new hooks to do it differently in such short timespan. Perhaps towards v.3 is more appropriate? |
I'm fine to land your PR as a short-term workaround. But the complexity and counter-intuition on how to use the |
BTW, I think |
I'd like to implement #397 for that. |
IMO it's ok to release a minor version, because we are not breaking backwards compatiblity. That's the semver way. It also allows us to release a major version when we learn enough about the new hooks and find issues that require a major change. |
Clarification: I was not questioning what have been released. I meant to say if we fix the hooks with breaking changes, it's reasonable to release it together with the promise as a major release. |
I don't want to repeat all my arguments why I still think the current design of operation hooks is good, I feel we are running in circles here. Let's land this pull request to fix the @fabien's immediate problems (after the commit history is cleaned up) and start a new discussion on what is a better solution for this kind of hooks, hopefully getting more community input this time. |
👍, makes sense. |
Because there are subtle differences between "full update of an instance", "updateAttributes" and "updateAll". However, I am ok with adding property getters |
@bajtos I'll clean it up so it can be merged. Thanks! 👍 |
For |
@raymondfeng is there anything else to discuss in this PR? Can I merge it once @fabien cleans up the commit history? |
Go ahead pls. |
@bajtos I'll push the code in the morning ... sorry about the delay! |
a08047b
to
74ad455
Compare
@bajtos cleaned up - can you merge please? |
@fabien can you please beef up the commit message and explain the changes? If not then I'll try to do that myself tomorrow, so that this is finally landed. |
Landed via bc54104 |
@bajtos I'm a bit late to the party - thanks for landing this patch and beefing up the commit! |
You are welcome, thank you for your patience :) I'd like to wait with the release until #452 is landed too, so that we don't release minor versions too often. |
For consistency, all instance-level operations (
prototype.updateAttributes
andprototype.delete
) will now have access to the current instance as part of the hook'scontext
:prototype.save
+before save
: the instance will be available asinstance
(unchanged behavior).prototype.save
+after save
: the instance will be available asinstance
(unchanged behavior).prototype.updateAttributes
+before save
: because the instance should not be manipulated directly, the instance will be available as currentInstance to distinguish between theinstance
that can be directly modified (it is omitted now). In other words,currentInstance
provides the current 'state' of the instance, and it should be regarded as immutable. Any modifications should be applied tocontext.data
, which incorporates the partial nature of theupdateAttributes
operation.prototype.updateAttributes
+after save
: in after hooks, this difference is not important, and thus, the instance will be simply be available asinstance
.prototype.remove/destroy/delete
+before save
: again, the difference is irrelevant, so the instance will be available asinstance
.prototype.remove/destroy/delete
+after save
: the instance will be available asinstance
.