-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat(repository): hasOne relation #1879
Conversation
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.
Great start!
* @param options Options for the operations | ||
* @returns A promise of the target object or null if not found. | ||
*/ | ||
findOne(filter?: Filter<Target>, options?: Options): Promise<Target | null>; |
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.
Let's replace find
and findOne
methods with a single method called get
.
get(options?: Options): Promise<Target | undefined>;
Since there is always at most one target model instance associated with the source model instance, it feels more natural for me to get
that single instance than findOne
. I am also not sure what the filter
argument would be good for, because I see very little value in filtering a set of at most single item.
On the second thought, maybe we should use find(option)
instead of get(options)
to indicate that the method may return undefined
when the target does not exist, as opposed to throwing an EntityNotFound exception.
I think we should also consider the alternative where get(options)
actually throws when the target model was not found, because that's what CRUD repository methods findById
, patchById
, etc. do.
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.
Yeah I thought about not needing filter
argument too, then thought that someone might want to use the fields
key in filter
to get the instance with certain properties (maybe not a good use case).
I think we should also consider the alternative where get(options) actually throws when the target model was not found, because that's what CRUD repository methods findById, patchById, etc. do.
These methods throw the EntityNotFound
error which would require an id
to be present and in this case we won't be given one for the method. For that reason, I think naming it get(options)
and returning undefined
sounds like a solid approach IMO.
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.
Yeah I thought about not needing filter argument too, then thought that someone might want to use the fields key in filter to get the instance with certain properties (maybe not a good use case).
That's a good use case. Can we mark filter
as accepting Filter
properties without where
? See Exclude<T, U>
in TypeScript's Advanced Types.
These methods throw the EntityNotFound error which would require an id to be present
I have run into this problem while encountering belongsTo
relation too. I think it's a clear sign that we need a way how to improve EntityNotFoundError
to support a where filter in addition to the id value. Such change would be out of scope of this pull request though.
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.
That's a good use case. Can we mark filter as accepting Filter properties without where? See Exclude<T, U> in TypeScript's Advanced Types.
Yes, this approach sounds good to me 👍 and for EntityNotFound
, we can create a follow-up task for those enhancements.
): Promise<TargetEntity> { | ||
const targetRepository = await this.getTargetRepository(); | ||
return targetRepository.create( | ||
constrainDataObject(targetModelData, this.constraint), |
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.
I am afraid this is not enough to ensure there is never more than one model linked.
Please add a test that calls .create()
two times and verifies that the second create is rejected.
This is the main difference between HasOne and HasMany.
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.
Great catch; I'm working on this right now, and was wondering, where we should have validation in place to reject subsequent requests. If we do it at this level, then we'd probably have to make a query first to see if there is an existing instance IIUC. Thoughts?
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.
Could you please check how is HasOne implemented in LB 3.x?
/** | ||
* The foreign key used by the target model. | ||
* | ||
* E.g. when a Customer has many Order instances, then keyTo is "customerId". |
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.
"has one", not "has many"
* @param options Options for the operations | ||
* @returns A promise of the target object or null if not found. | ||
*/ | ||
findOne(filter?: Filter<T>, options?: Options): Promise<T | null>; |
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.
We should be using undefined
instead of null
because it works better with ES6 default parameters.
This is scary! I don't think your changes would account for such a huge drop in code coverage numbers, but at the same time we will need to fix this before landing your PR. |
Yeah that is huge indeed 😨. I will take a look at the details. |
c358770
to
2b6221b
Compare
'HasOne relation does not allow creation of more than one target model instance', | ||
); | ||
} else { | ||
return await targetRepository.create( |
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 crucial to leverage an atomic implementation of findOrCreate
provided by our connectors. The current proposal is prone to race conditions, where to instances of the target "hasOne" model can be created.
Consider the following scenario: the LB4 server is handling two incoming HTTP requests to create a hasOne target and the code is executed in the following way by Node.js runtime:
- Request 2 arrives,
DefaultHasOneRepository#create
is invoked targetRepository.find
is called. It's an async function so other stuff happens while the query is in progress.- Request 1 arrives,
DefaultHasOneRepository#create
is invoked. targetRepository.find
is called. It's an async function so other stuff happens while the query is in progress.targetRepository.find
for Request 1 returns, no model was found.targetRepository.create
in called.targetRepository.find
for Request 2 returns, no model was found.targetRepository.create
in called.- Both
create
requests eventually finish. We have two models that are a target ofhasOne
relation. 💥 💣 💥
I am proposing to put this pull request on hold and open a new pull request to add findOrCreate
method to EntityCrudRepository
(and the implementations).
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.
Thank you for explaining the need for an atomic implementation with a great example. I agree, and I agree with doing that first.
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.
I opened a new issue to keep track of "findOrCreate" story, see #1956
|
||
const sourceModel = relationMeta.source; | ||
if (!sourceModel || !sourceModel.modelName) { | ||
const reason = 'source model must be defined'; |
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.
Is it ok to define reason
twice in one function?
The first one is on https://github.com/strongloop/loopback-next/pull/1879/files#diff-a42257687a98a97022f7eb63398d2269R68
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.
Yes, it's block-scoped.
throw new InvalidRelationError(reason, relationMeta); | ||
} | ||
|
||
return Object.assign(relationMeta, {keyTo: defaultFkName}); |
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.
Do we allow override the default fk in the relationMeta
? This line implies we don't?
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.
Good catch. I believe we should allow users to provide their own keyTo
value. @b-admike please add a test to ensure this use-case is supported.
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.
Actually: this line is executed only when relationMeta.keyTo
was falsey, see lines 72-75 above.
if (relationMeta.keyTo) {
// The explict cast is needed because of a limitation of type inference
return relationMeta as HasOneResolvedDefinition;
}
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.
@b-admike Great effort! The implementation looks similar to hasMany relation, from the design's perspective, would it be possible to make hasOne
a more constrained version of hasMany
? IMO essentially hasOne
= hasMany
+ one more constraint in the create
method.
Some proposal:
- Add a configure property in the
hasMany
relation metadata to specify one-to-one relation. - Apply additional check in the hasManyRepository's
create
method to make sure one source instance only HAS ONE target instance.
In this case hasOne
is like a sugar relation of hasMany
.
Thought?
@jannyHou Thank you for your valuable feedback. I do intend to refactor the code since these two relations have lots of similarities, but thought it would be better to start off like this and think about ways we can re-use common code.
I like this proposal. Are you thinking that the |
Let's explore! Few constraints to keep in mind:
|
Cross-posting from #1956 (comment) |
2b6221b
to
fd03a78
Compare
Hello @b-admike, I simulated a use case where we have a "hasOne" relation connection from a source model (parent) to a target model (child). Missing features:
I am currently preparing a short documentation that details how to code an application example of "hasOne" relation. Please let me know where do you think that I can post it. |
hi @RaphaelDrai thank you for taking the time to test out this branch and compiling your results. I've actually continued my work on the
This should now be a
That's awesome. We can possibly use the documentation as a follow-up or part of #2005 for the relation I added to |
Closing this in favour of #2005. Feel free to also copy/link your comment there @RaphaelDrai. |
Hello @b-admike ,
|
First iteration of
hasOne
relation. This PR includes the the repository factory, repository interface and default hasOne repository, and has one decorator implementations (mostly taken from our hasMany implementation since they're very similar in nature). It also includes an acceptance test with a Customer has one Address relation to drive it and a unit test as well. Please note that only the EDITget
andcreate
methods are implemented at the moment and tests covering them, so that we can focus on that and incrementally add patch and delete methods to the hasOne repository interface.Todos
hasOne
relationhasOne
relationFixes #1422
Checklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated