Search code examples
elixirecto

Back an ecto model with custom sql instead of a table


I've got this situation where I generate an intermediate table with some custom sql. In my app I have a model that points at that intermediate table. I've got this requirement to not use an intermediate table, so I'm trying to figure out how to write an Ecto model in such a way that it uses that custom sql on loading data for that model.

IMPORTANT: this model is only used for reading from that table or sql select results, so I don't have to support insert/update/delete. That should vastly simplify what I'm trying to do.

Here's a fake model for the purposes of what I want to do:

defmodule EventBridge.C3poEvent do
  use Ecto.Schema
  import Ecto
  import Ecto.Query, only: [from: 1, from: 2]

  schema intermediate_table_name do
    field :id, :integer
    field :access_dates, :string
    field :action, :string                 
    field :counter, :integer
  end
end

Let's assume this sql to get the data:

select id, access_dates, action, counter from some_other_table 
  where some_conditions = true;

What I need to be able to do is load models using that sql rather than from the table my example is backed by.

In my head I was thinking that I'd put a function in the model like:

def get_model(Model, some_conditions) do
  ...
end

and in that function just manually load the model with the sql in there. But I'm not convinced this a) makes sense or b) will result in a model I can use to access the fields.

Perhaps I shouldn't even be using a model? Just a custom struct that has the get_model method in it and not worry about backing it with the schema?

again, please not that I'm only reading this data ever.


Solution

  • If I properly understood your need, you just need an “upgraded” struct of a kind. That is easily achievable with Ecto.Schema.embedded_schema/1. I would provide an adopted from my production code example showing validations and other Ecto goodness:

    defmodule EventBridge.C3poEvent do
      use Ecto.Schema
      import Ecto.Changeset
    
      @required_fields ~w|id access_dates action|
      @fields ["counter" | @required_fields]
    
      @primary_key false
      embedded_schema do
        field :id, :integer
        field :access_dates, :string
        field :action, :string                 
        field :counter, :integer
      end
    
      def new(data) when is_map(data) do
        %__MODULE__{}
        |> cast(data, @fields)                 # free from Ecto
        |> validate_required(@required_fields) # free from Ecto
        |> apply_changes()                     # free from Ecto
      end
    
      ## `to_string` / interpolation support
      defimpl String.Chars, for: EventBridge.C3poEvent do
        def to_string(term) do
          "🎫<[📅#{access_dates} 🎬#{action}(#{counter})]>"
        end
      end
    
      ## `inspect` support
      defimpl Inspect, for: EventBridge.C3poEvent do
        import Inspect.Algebra
    
        def inspect(%{
            id: id,
            access_dates: access_dates,
            action: action,
            counter: counter}, opts) do
          inner = [id: id, access_dates: access_dates,
                   action: action, counter: counter]
          concat ["#EventBridge.C3poEvent<", to_doc(inner, opts), ">"]
        end
      end
    end
    

    The above is ready-to-use scaffold for any “upgraded” struct backed by Ecto. Once you have decided to store it in the database, just switch from embedded_schema to schema and you are all set.