3
votes

I'm having a hard time trying to achieve polymorphism in Ecto. My main problem is that i'm trying to handle polymorphic association in Phoenix, but with data coming from a database used and created by our - let's be honnest, a little old - main app, which is based on Rails.

Every other models are handled well and there's no problem to read or write data on both Rails and Phoenix's side.

Except for those with polymorphism. I spent a lot of time reading though Ecto's documentation and i get that the "rails way" is not the "right way" but i'm kind of stuck here, because my models are already written on the rails side (some of theme already store millions of rows).

So i have this abstract model element, in Rails, which represents 7 tables (image, video, excel, etc..). And i have two other tables, doc_images and items. Those two other tables belongs_to element and therefore have the rails magical fields element_id and element_type. Again, no element table exists in postgres, only images, videos, etc.

I've tried to setup my images and videos models in Phoenix, but when i had this code into my items schema :

  schema "items" do
    belongs_to(:video, Video, foreign_key: :element_id)
    belongs_to(:image, Image, foreign_key: :element_id)
    field(:element_type, :string)
    timestamps([{:created_at, :updated_at}])
  end

I can't compile because of (ArgumentError) field/association :element_id is already set on schema.

I was okay with doing all queries by hand, but i can't seem to be able to write the schemas right.

In the end, i need to be able to make those kind of queries work (all of it is working perfectly fine except for the doc/video/image part, obviously) :

MyApp.Repo.one(from t in Token,
      join: p in assoc(t, :project),
      join: vc in assoc(p, :vcard),
      join: th in assoc(p, :theme),
      left_join: z in assoc(p, :zip_archive), on: p.id == z.project_id,
      join: c in assoc(p, :company),
      join: b in assoc(c, :binfos),
      left_join: pi in assoc(p, :project_items), on: pi.id == pi.project_id,
      left_join: i in assoc(pi, :item),
      left_join: d in assoc(i, :doc),
      left_join: v in assoc(i, :video),
      left_join: im in assoc(i, :image),
      left_join: di in assoc(d, :doc_image),
      where: t.token_url == ^token_url,
      preload: [project: [:vcard, :theme, :zip_archive, [company: :binfos], project_items: [item: [doc: :doc_image, :video, :image]]]])

So my question is : what other options do i have ? I can add columns and even other tables to my database (and migrate datas from the existing tables in it) but i can't remove the element logic from my Rails app (mostly for time concern and because the other guys in my team love the Rails way :p).

Has anyone been able to handle polymorphism in Ecto the Rails way, event if it's strongly discouraged ?

1

1 Answers

1
votes

Instead of trying to replicate behavior, I’d go with the simplest and cleanest solution instead.

Get rid of any belongs_to in your items.

schema "items" do
  field :element_id, :integer
  field :element_type, :string
  ...
end

Add validation to changeset, checking that the respective element_type with this element_id exists.

Select from Token joined everything, save for doc/image/etc zoo.

Select from element_type by element_id.

This would surely work and despite it implies an additional query per both select and upsert, I am positive the clarity of the resulting code would pay back soon.

Now do your normal query without joining