I need basically to generate a random string to be inserted as value for a field called url_prefix in the DB on new record creation.
I want to ensure that the generated string is unique i.e. is not associated with any record in the DB’s table.
So I want to generate it, test if it is not already stored in DB, then make the INSERT query.
I have to instruct Elixir to keep generating a random string until the string on hand is unique.
Other languages has while statements which could be used for such scenario. How to do this in Elixir?
All this in the context of Phoenix.
Implement an infinite stream of id‘s and use reduce_while to reduce over it until a db insert is successful.
1 Like
You could do this in the db, assuming piostgres
defmodule App.Repo.Migrations.AddUUIDToSomeRecord do
use Ecto.Migration
def change do
create table(:some_records) do
add(:random_id_field, :uuid, default: fragment("uuid_generate_v4()"))
end
end
end
4 Likes
Otherwise, I’d do it recursively
defmodule MyMod do
def try_generate_and_save_thing(:try_again, data) do
random = random_string(64)
case SomeOtherMod.save(data, random) do
true ->
:done
false ->
:try_again
end
|> try_generate_and_save_thing(data)
end
def try_generate_and_save_thing(data, :done), do: {:ok, data}
defp random_string(length) do
:crypto.strong_rand_bytes(length) |> Base.url_encode64 |> binary_part(0, length)
end
end
4 Likes
NobbZ
August 28, 2019, 5:43pm
5
This is not an atomic operation, therefore you need to either find a way to do it on the database, or acquire a full table lock.
Anyway, then it’s either the infinite stream or recursion.
4 Likes
Doing in the db seems like it’s probably the best choice for most use cases, IMO.
5 Likes
blatyo
August 29, 2019, 1:23am
7
Maybe a UUID would work better
3 Likes
I did it in the database using built in function and the value gets generated in the field in the database but Phoenix is not aware of that value, returns it to the front-end as null
on creation.
NobbZ
August 29, 2019, 7:59am
9
Yes, this is how databases work… You’ll need to read after write.
1 Like
Kurisu
August 29, 2019, 8:23am
10
So maybe the UUID suggestion will work for you if you need to get the value before the insert.
Something like:
def changeset(struct, params \\ %{}) do
struct
|> Map.update(:uuid, Ecto.UUID.generate,
fn value -> value || Ecto.UUID.generate end)
|> ....
end
1 Like
Ecto has built in support for reading fields after writes, when using postgres:
field :id, :uuid, primary_key: true, read_after_writes: true
11 Likes
This is an excellent approach
Recently I needed to get uuid before saving to database, for use with Arc Ecto. I did not set uuid in the db, but in the changeset.
@primary_key {:id, :binary_id, autogenerate: false}
@foreign_key_type Ecto.UUID
schema "pictures" do
...
end
def changeset(picture, attrs) do
picture
|> cast(attrs, [:name])
|> ensure_uuid(:id)
|> cast_attachments(attrs, [:display_image])
|> validate_required([:name, :display_image])
end
defp ensure_uuid(changeset, field) do
case get_field(changeset, field) do
nil -> changeset |> put_change(field, Ecto.UUID.generate())
_ -> changeset
end
end
This way I could get uuid before saving to db, and use it to generate an Arc Ecto local storage path. It is working well in this situation.
2 Likes