Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

different embed schemas for the same model (possible pull request) #1001

Closed
troyk opened this issue Oct 9, 2015 · 3 comments
Closed

different embed schemas for the same model (possible pull request) #1001

troyk opened this issue Oct 9, 2015 · 3 comments

Comments

@troyk
Copy link

troyk commented Oct 9, 2015

I have the following schema:

defmodule AH.SearchReq do
  use AH.Web, :model

  schema "search_reqs" do
    field :product_id, :string
    field :request, :map
    field :result, :map
  end
end

I want to use different embed schema's based on the value of product_id and embed them in the request and result :map fields. At first, I thought the after_load callback would work because I could do a cond on the product_id and return a different model. But I get this error:

** (exit) an exception was raised:
    ** (ArgumentError) expected `after_load` callbacks to return a AH.SearchReq, got: %AH.SearchReq.CRCT{__meta__: #Ecto.Schema.Metadata<:built>, ...}
        (ecto) lib/ecto/model/callbacks.ex:319: Ecto.Model.Callbacks.__apply__/3
        (ecto) lib/ecto/adapters/sql.ex:503: anonymous fn/3 in Ecto.Adapters.SQL.process_row/3
        (elixir) lib/enum.ex:1102: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
        ...

If there is not a good way to do this, any interest in a pull-request to make after_load not require the same model to be returned? There does not appear to be any code after the Schema.__load__/6 that depends on the same model being and while I understand the other callbacks require a changeset to be returned, is there really a reason to ensure after_load keeps the same model?

  def __load__(model, prefix, source, context, data, loader) do
    source = source || model.__schema__(:source)
    struct = model.__struct__()
    fields = model.__schema__(:types)

    loaded = do_load(struct, fields, data, loader)
    loaded = Map.put(loaded, :__meta__,
                     %Metadata{state: :loaded, source: {prefix, source}, context: context})
    Ecto.Model.Callbacks.__apply__(model, :after_load, loaded)
  end
@josevalim
Copy link
Member

You are describing a solution. Can you please describe which problem you are trying to solve? Why do you need different data that looks exactly the same in Elixir? At first, allowing models to become a different struct after load does not sound like a good idea and can potentially be quite confusing.

@troyk
Copy link
Author

troyk commented Oct 9, 2015

@josevalim agreed, would be cool if I could just override Schema.__load__/6 somehow.

use case:

request and result are :map, in the db they are postgresql jsonb columns. I want to take advantage of Ecto's type casting, changeset features on the data in the :map fields, because each product_id is going to have the same request/result schema. In my rails app I use STI and have a getter on each class for the request/response attributes. Something like:

class SearchReq < ActiveRecord::Base
end

class SearchReq::CRCT < SearchReq
  class Request
    # some attribs
  end
  def request
    @request ||= Request.new(super)
  end
end

class SearchReq::CRST < SearchReq
  class Request
    # some other attribs
  end
  def request
    @request ||= Request.new(super)
  end
end

Ecto embeds is very cool, but in it's current form it's very limited to only modeling the same data in each field, where I see a huge benefit is being able to put varying types of data in the same table. A good example of this is a items table in an ecommerce system. Each item may have a price, inventory count, shipping weight, etc. But a clothing item will need attributes such as color, sizes, etc while a computer item will have CPU, RAM, HDD, etc. So it would be real neat to do something like:

defmodule Order do
  use Ecto.Model
  schema "orders" do
    embeds_many :items, Item.Clothing, Item.Computer
  end
end

defmodule Item.Clothing do
  use Ecto.model

  embedded_schema do
    field :color
  end
end

defmodule Item.Computer do
  use Ecto.model

  embedded_schema do
    field :cpu
  end
end

Back to me SearchReq model, one idea I'm throwing around is have a SearchReq.to_product_searchreq fn that will return SearchReq.CRCT, SearchReq.CRST etc. Maybe even store it in a virtual field.

Thanks,

Troy

@josevalim
Copy link
Member

Ok, that makes a bit more sense. However I am thinking that, if you want to store anything in the field, then you don't want an embed but keep it as a map (or array of maps) and manage your own structs. The reason here is that Ecto does not store model information in embed so we wouldn't know which model to serialize it back too. Luckily, if you drop Ecto in favor of a map, you can easily make it whatever you want. You can even use a custom type that does all the encoding/decoding for you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants