Custom primary key column... can we alias it using "source"?

Using Ecto, I’ve been able to attach to legacy database tables and I’ve been able to define a custom primary key, e.g. “something_id”, but the name of the key doesn’t get aliased as “id”. This behavior is different than the other fields defined using the “scope” option.

Consider the following modules:

defmodule Something.SomethingCtx.SomethingBase do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      @primary_key {:legacy_id, :integer, autogenerate: false}

defmodule Something.SomethingCtx.Something do
  use Something.SomethingCtx.SomethingBase
  import Ecto.Changeset
  alias Something.SomethingCtx.Something

  @derive {Phoenix.Param, key: :legacy_id}

  # Weird table name
  schema "legacy_weird_tablename" do
    field :foo, :integer, [source: :legacy_foo]
    field :bar, :string, [source: :poorly_named_column]

  @doc false
  def changeset(%Something{} = menuitem, attrs) do
    |> cast(attrs, [:legacy_id, :foo, :bar])
    |> validate_required([:legacy_id, :foo, :bar])

By using the “source” option, now I can internally refer to the legacy column names using a nice, short, name (:foo and :bar in the above example, instead of legacy_foo or poorly_named_column.

However, I still need to use “legacy_id”. E.g. in my JSON view, I have to reference the something.legacy_id property instead of having a nice “id” alias.

Is it possible to “alias” the custom primary key?
(And secondarily, did I do the above correctly? I’m pretty shaky here, but so far I’ve been able to attach to legacy database tables using code like the above).


Well, I figured out my own problem, so I’m going to post the solution here as a reference.

First, creating the “base” module was not necessary – although the Ecto docs demonstrate that technique for other cases, it’s not necessary just because your database table uses a non-standard primary key. Here’s what the module looks like, pared down for simplicity:

defmodule MyApp.MyCtx.Myschema do
  use Ecto.Schema
  import Ecto.Changeset
  @primary_key {:something_id, :integer, autogenerate: false, source: :something_id}
  @derive {Phoenix.Param, key: :something_id}

  schema "legacy_table_name" do
    field :id, :integer, [source: :something_id]  # <--- seems redundant, but it exposes the primary key column as "id"
    field :foo, :string, [source: :legacy_foo]
    # ... etc ...

  # ...changeset, etc...

Adding the @primary_key {...} bit makes things work, but your views will still need to be adjusted because the record object won’t have a standard “id” property. So the trick was to simply add “id” as a field and reference its “source” column like we do for all the other fields. That way your views can use the “id” property as expected (the legacy something_id property will be there too, but meh… it’s not hurting anything)

Hope that my solution represents a valid approach – it seems to have worked.

1 Like

Seems good to me. :slight_smile:

I’m wondering, would it not work by doing:

  @primary_key {:id, :integer, autogenerate: false, source: :something_id}

If not then I’m curious if that’s a bug in Ecto?

1 Like

Ah, that does work, thank you! That’s much simpler.

I think it might be nice to add a couple more examples to the Ecto docs for these types of use-cases. Surely I’m not the only one who was confused by this and failed to arrive at a “best-practices” solution. Thoughts?


You should submit a PR! They especially love documentation PR’s. :slight_smile: