Should I use old-fashioned DB lookup tables? What might be some alternatives?

I realize not all mapped values are the same. Some have a very small set number of KVs (int id => 5-point scale). Some have a larger (int id => country code). Some have an indefinite set of KVs. In which cases (if not all) would you recommend DB lookup tables with Ash relationships. Would you recommend an alternative? I had considered for small sets to use a calculation, but I think that works with reads but not writes, right?
6 Replies
ZachDaniel
ZachDaniel2y ago
Its hard to say really. I'd probably have to choose situationally. I don't think size is really the constraint, its more like editability I'd use lookup tables if I want to be able to dynamically at runtime change the list of things For country codes, for example, I'd use an enum because its static, people aren't creating more country codes
Korbin
KorbinOP2y ago
Okay, Ash.Type.Enum is a good callout for certain situations. Is there an example of that usage (didn't see one in ash_hq) and how it works with Postgres?
ZachDaniel
ZachDaniel2y ago
It doesn't create an enum type in postgres migrations or anything like that it is just represented as a string under the hood use Ash.Type.Enum, values: [:foo, :bar, :baz] a string in postgres I mean
Korbin
KorbinOP2y ago
I see. I wish there was a callback or something to specify underlying value. Something like:
:high => 3
:med => 2
:low => 1
:high => 3
:med => 2
:low => 1
That'd make Enum basically a complete in-code replacement for a static lookup table in the DB. I suppose there's probably an escape hatch in Ash for something like this though. Be it in a code_interface or custom action or something like that.
ZachDaniel
ZachDaniel2y ago
You could do that with a custom type pretty easily, but I'm curious why you'd want to bother with having a mapping at all and not just using the value But you could do something like this:
defmodule YourThing do
use Ash.Type

@mapping %{
:high => 3,
:med => 2,
:low => 1
}

@reverse_mapping Map.new(@mapping, fn {k, v} -> {v, k} end)

def storage_type, do: :integer

def cast_input(nil), do: {:ok, nil}
def cast_input(value) do
if Map.has_key?(@mapping, value) do
{:ok, value}
else
:error
end
end

def cast_stored(nil), do: {:ok, nil}
def cast_stored(value), do: Map.fetch(@reverse_mapping, value)

def dump_to_native(nil), do: {:ok, nil}
def dump_to_native(value), do: Map.fetch(@mapping, value)
end
defmodule YourThing do
use Ash.Type

@mapping %{
:high => 3,
:med => 2,
:low => 1
}

@reverse_mapping Map.new(@mapping, fn {k, v} -> {v, k} end)

def storage_type, do: :integer

def cast_input(nil), do: {:ok, nil}
def cast_input(value) do
if Map.has_key?(@mapping, value) do
{:ok, value}
else
:error
end
end

def cast_stored(nil), do: {:ok, nil}
def cast_stored(value), do: Map.fetch(@reverse_mapping, value)

def dump_to_native(nil), do: {:ok, nil}
def dump_to_native(value), do: Map.fetch(@mapping, value)
end
Korbin
KorbinOP2y ago
Yeah, I think custom types is exactly what I'm looking for. I totally missed that section in the docs. TLDR; Yes, you should probably in many cases still use old-fashioned lookup tables. This gives the benefit of run-time editability and additions of values. For key/value mappings that are super-definitely static, two options exist: Ash.Type.Enum and making your own custom type. For Enums, the atom is converted to a string (:ci_string I think) and stored in DB like that. This can fall prey to larger table storage as you're storing copies of each string as opposed to an ID pointing at the same value. Custom types can help with this since you can define two-way translations.

Did you find this page helpful?