-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Add Repo.reload/2 and Repo.reload!/2 #3416
Conversation
lib/ecto/repo/queryable.ex
Outdated
case schema.__schema__(:primary_key) do | ||
[pk] -> | ||
keys = Enum.map(structs, &get_pk!(&1, pk)) | ||
preloads = Keyword.get(opts, :preload, []) |
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.
Honestly, I don't think we need to support this option. Doing a |> Repo.preload(...)
after is only very few characters more verbose than , preload:
.
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.
@josevalim wouldn't |> Repo.preload()
imply another trip to the database while from(..., preload: ...)
does it in one pass? Or they're always equivalent?
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.
Preload always issues separates queries, that’s what it is about, so both forms are equivalent!
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 nice to know! I thought the form in Ecto.Query
would optimize into a join of some kind
lib/ecto/repo/queryable.ex
Outdated
if result_count < expected_count do | ||
raise Ecto.TooFewResultsError, queryable: query, min: expected_count, count: result_count | ||
end | ||
|
||
result |
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.
Unfortunately there is no guarantee result
will be in the same order as structs
. So you need to do something like this:
for struct <- structs do
struct_pk = Map.fetch!(struct, pk)
Enum.find(result, &Map.fetch!(&1, pk) == struct_pk) || raise "could not find"
end
This means you don't need the Enum.count nor the exception above, as the above will guarantee all entries have a match.
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.
If we guarantee same ordering in reload!/2
, should we also do the same for reload/2
?
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 should be for both!
lib/ecto/repo/queryable.ex
Outdated
:ok | ||
|
||
rejected -> | ||
raise ArgumentError, "Expected a struct or a list of structs, received #{inspect(rejected)}" |
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.
Messages should start in lowercase.
lib/ecto/repo/queryable.ex
Outdated
defp is_struct?(%{__struct__: _, __meta__: _}), do: true | ||
defp is_struct?(_), do: false |
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 typically use is_
or ?
but not both. In this case, since it is not a guard, ?
works better. :)
defp is_struct?(%{__struct__: _, __meta__: _}), do: true | |
defp is_struct?(_), do: false | |
defp schema?(%{__struct__: _, __meta__: _}), do: true | |
defp schema?(_), do: false |
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.
@josevalim I have a conceptual question: does Map.put(%{}, :__struct__, ...)
imply that the result is a struct?
In my conception, matching on %_{__meta__: _}
would be a little bit more semantic in this regard, but I really don't know if there is a practical difference
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.
This is a subset match, so you can match as above, but you shouldn’t build the struct manually as you may miss some fields. In any case your syntax is definitely nicer!
It is looking great! I have dropped some comments. |
Thanks, @josevalim, I'll fix those ASAP :) |
lib/ecto/repo/queryable.ex
Outdated
struct | ||
|> Map.fetch!(pk) | ||
|> case do | ||
nil -> | ||
raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs, found a `nil` id" | ||
key -> | ||
key | ||
end |
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.
Would this be more concise?
struct | |
|> Map.fetch!(pk) | |
|> case do | |
nil -> | |
raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs, found a `nil` id" | |
key -> | |
key | |
end | |
case struct do | |
%{^pk => key} when key != nil -> key | |
_ -> raise ArgumentError, "Ecto.Repo.reload/2 expects existent structs" | |
end |
lib/ecto/repo/queryable.ex
Outdated
@@ -485,4 +536,28 @@ defmodule Ecto.Repo.Queryable do | |||
query: query, | |||
message: "expected a from expression with a schema" | |||
end | |||
|
|||
defp assert_structs!(structs) when is_list(structs) 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.
Shall we guard against the case when users input different struct types in the list?
Repo.reload([%Post{}, %Comment{}])
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'll cover this case
test/ecto/repo_test.exs
Outdated
@@ -220,6 +220,33 @@ defmodule Ecto.RepoTest do | |||
end | |||
end | |||
|
|||
describe "reload" do | |||
test "raises when input have a `nil` pk" 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.
test "raises when input have a `nil` pk" do | |
test "raises when input structs do not have valid primary keys" do |
maybe?
lib/ecto/repo/queryable.ex
Outdated
if result_count < expected_count do | ||
raise Ecto.TooFewResultsError, queryable: query, min: expected_count, count: result_count | ||
end | ||
|
||
result |
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.
If we guarantee same ordering in reload!/2
, should we also do the same for reload/2
?
lib/ecto/repo/queryable.ex
Outdated
@@ -428,6 +459,26 @@ defmodule Ecto.Repo.Queryable do | |||
Query.where(queryable, [], ^Enum.to_list(clauses)) | |||
end | |||
|
|||
defp query_for_reload([head| _] = structs, opts) 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.
defp query_for_reload([head| _] = structs, opts) do | |
defp query_for_reload([%schema{} | _] = structs, opts) do |
maybe? Worse error message in case of a list of normal maps however.
Hi! I just fixed most of the suggestions. I think there is room for improvement in the code, but I didn't figure out how to do it. |
@josevalim let me know if you wanna merge without a squash, I may improve the commits to make a better history :) |
Bump :) (just in case you lost the notification) |
lib/ecto/repo.ex
Outdated
* `:prefix` - The prefix to run the query on (such as the schema path | ||
in Postgres or the database in MySQL). This will be applied to all `from` | ||
and `join`s in the query that did not have a prefix previously given | ||
either via the `:prefix` option on `join`/`from` or via `@schema_prefix` | ||
in the schema. For more information see the "Query Prefix" section of the | ||
`Ecto.Query` 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.
I don't think we should accept this option, rather, we should use the prefix from the structs themselves. You can find it under the __meta__
field.
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.
You can test this by using Ecto.put_meta(struct, :prefix, "another")
in test/ecto/repo_test.exs and then using assert_receive to check to which prefix the query went to. :)
If that doesn't work, have an integration test that sets Ecto.put_meta(struct, :prefix, "invalid")
and try to reload it and see that it will fail.
lib/ecto/repo.ex
Outdated
When using with lists, ordering is guaranteed to be kept. Results not found in | ||
the database will be returned as `nil`. |
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.
When using with lists, ordering is guaranteed to be kept. Results not found in | |
the database will be returned as `nil`. | |
When using with lists, it is expected that all of the structs in the list belong | |
to the same schema. Ordering is guaranteed to be kept. Results not found in the | |
database will be returned as `nil`. |
lib/ecto/repo/queryable.ex
Outdated
with [] <- Enum.reject(structs, &schema?/1), | ||
true <- Enum.all?(structs, &(&1.__struct__ == head.__struct__)) do | ||
:ok | ||
else | ||
rejected when is_list(rejected) -> | ||
raise ArgumentError, "expected a struct or a list of structs, received #{inspect(rejected)}" | ||
|
||
false -> | ||
raise ArgumentError, "expected an homogenous list, received different struct types" | ||
end |
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.
This can be clearer as conditionals:
if not Enum.all?(structs, &schema?/1) do
raise ArgumentError, "expected a struct or a list of structs, received #{inspect(rejected)}"
end
if not Enum.all?(structs, &(&1.__struct__ == head.__struct__)) do
raise ArgumentError, "expected an homogenous list, received different struct types"
end
:ok
lib/ecto/repo/queryable.ex
Outdated
end | ||
end | ||
|
||
defp query_for_reload(struct), do: query_for_reload([struct]) |
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 would remove this clause and call it as query_for_reload([struct])
in the reload
/reload!
functions. :)
Awesome work @v0idpwn! I have added some minor comments and a note about prefix handling. In particular, I think you need to do this:
I have added some notes on how to write the test for it. |
Just pushed those fixes :) |
💚 💙 💜 💛 ❤️ |
Closes #3389
I'm still polishing, got to fix some things regarding code quality. Still, the API proposal is this. Do I need to change something?