Convert string of schema to schema

I have a map looks like this

%{"type" => "Elixir.Dasbhoard.Users.User", "id" => "3"}

And I need to fetch this

%{"type" => type, "id" => id}

type
|> where(id: ^id)
|> Repo.one()

How can I convert this string type to schema?
For example
"Elixir.Dashboard.Users.User" to Dashboard.Users.User
So I can use this for query.

That’s a bad practice. You shouldn’t store Elixir module names in database. I see you tried to create a polymorphic association, but that’s definitely wrong way. As of current state of ecto you should create many fields in which only one of them would not be nil.

schema "table_name" do
  belongs_to :user, Dasbhoard.Users.User
  # and so on …
end

You then need to determine which one would belong to User or to other schema:

polym_child =
  case parent do
    %{user_id: user_id} when not is_nil(user_id) -> parent |> Ecto.assoc(:user) |> Repo.one!()
    # same for other polymorph fields
  end

Asking your question you need to call String.to_existing_atom(string). However said atom needs to exists. You can directly change to atom String.to_atom(string), but that’s not safe.

Maybe I needed to describe detail of my problem.
I don’t save elixir module name to the database. I need to pass information to Oban perform/1 function

And the type information is not static, it is dynamic. So I can’t pass only id to that function, I need to know also the schema(type) for example User, Profile or Address

So I can do something like this

def perform(%{args:%{"type" => type, "id" => id}}) do
  resource = get_resource(type, id)
  # rest of the code....
end

defp get_resource(type, id) do
  # TODO: Convert type
  type
  |> where(id: ^id)
  |> Repo.one()
end

I didn’t used oban yet, but from documentation I see that …

… you do, because looks like oban is using a database for working with queues.

Still using module looks bad here … Just think what would happen if application is stopped before said queue is empty and meanwhile you update your code. module may not be valid any longer.

If that doesn’t convince you think how much bytes you are sending to database and reading it before perform/1 call. A simple resolve_type/1 function would make it much better:

defp resolve_type("address"), do: Dasbhoard.Users.Address
defp resolve_type("profile"), do: Dasbhoard.Users.Profile
defp resolve_type("user"), do: Dasbhoard.Users.User
# …

In this case if you would rename any of those modules all you need to do in order to make your code work is to update specific resolve_type/1 function clause and rest of your code stays unchanged, for example, so by simply adding resolve_type/1 piped call your code would work without any further issues:

type
|> resolve_type()
|> where(id: ^id)
|> Repo.one()

All you need is String.from_existing_atom(type) and you’ll get the module. Take a look at Oban.Worker.from_string/1 for a safer version.

Why? I’ve had a project where we would store elixir module names that would be processing data, you have to be safe with renaming, but in general was a pretty nice approach for that case.

I’d advise against it because should you change the module names then you’ll have to do a data migration i.e. change those names in the DB. Having a set of shorter names is more flexible, and having a function resolve a key to a module name as @Eiji is doing is not hard or inefficient or anything of the sort.

Though a good team discipline – “worker module names are not to be changed” – can achieve the same results so really, at the end of the day it’s a team preference. :person_shrugging:

1 Like

Yeah, I was thinking about that, however there is no way to ensure unique names beside module names (at least not at compile-time), so I ditched the idea of refactoring.

Well, Ecto.Enum exists and I’ve used it for similar goals before, works pretty well, especially when you harness PostgreSQL’s support for enums then most of your concerns are taken care of.

Well there are several ways to skin this cat.

I wanted to leverage on elixir compile-time power when I was thinking about giving the modules that implemented the processing logic an alternative unique name that would not be connected to the module name, but I didn’t find a solution to handle this as the race condition when files are compiled cannot be solved with metaprogramming alone, hence there is no way to ensure that those names are unique.

Assuming that:

  1. We are talking about schema and then
  2. We are using Phoenix contexts

There should not be a problem with it, right?

For example Item may be hard name have a unique name, but Todo.Item is much easier, so we can have "todo_item", "menu_item" and so on … As long as we have group (here context) and last module part name (here schema) we can easily create a unique_name with a simple naming context_schema.