Updating an embedded_schema & general problems understanding Ecto

Hello all!

Yet again, I’ve run across a problem with Ecto: it seems to be my nemesis!

In this instance, I’m trying to build an admin area for a travel blog. The application allows a user to submit Posts, and Places. These are in two separate contexts (Content.Post & Locations.Place). The desired outcome is that when a user is submitting a Post they associate a Place through a Location.Visit.

In an effort to provide a nice editing experience, I’ve created another context called Admin which uses an “embedded_schema” for a Admin.Post model, which combines the required fields into one model.

This is to avoid using Phoenix’s nested forms, and also to leverage Ecto.Multi.

All of the above works correctly for creation of new Posts & Places. However, I can’t seem to use the same model/form to update them.

I’ve written a query to load the data from the various tables and load it back into an %Admin.Post{} struct, which I pass to the form, along with a changeset. When I try to submit updates, Phoenix tries to use a POST route rather than a PUT or a PATCH. I understand this is because the changeset doesn’t have a :loaded state because I built it manually (from it’s constituent models), so it infers that this should be a creation, not an update.

I’ve only been able to find examples of inserts with an embedded_schema (use case is usually something like “registration”, which obviously doesn’t require updates). Is something like this possible with “embedded_schemas”?


Now, the real problem here is that I just don’t seem to be able to get my head around associations with Ecto properly. I understand them well enough, and can build them so that they work perfectly well in iex. I’ve done this for multiple approaches: nested data structures where cast_assoc is used, a get_or_insert approach that uses put_assoc as explained in “What’s new in Ecto”, and another using embedded_schema and Ecto.Multi.

Where all of these fall apart for me is when I come to use them through Phoenix. The only approach that I’ve been able to get working is with nested forms and cast_assoc.

This has been an issue for me in every Elixir/Phoenix project that I’ve started over the last 18 months, with all of them bar one falling by the wayside. No matter how simple I make the problem, as soon as there’s a non-trivial association going on, I hit a wall.

I’ve hunted high and low for advice and guidance on this. Most tutorials or examples are necessarily simple, and don’t really try to solve the same problems I’m encountering. “What’s new in Ecto” has helped me learn a lot about Ecto, but I still haven’t got to a stage where I’m comfortable with it.

The best example I’ve found of the types of associations I’m trying to model is in the Changelog codebase, but from what I can tell, their approach uses nested forms, cast_assoc and a sprinkling of JS to achieve the desired outcome of managing other models (hosts and sponsors in their case) whilst adding an episode. I’m not adverse to this approach at all, but in the spirit of learning and understanding, I’m keen to see what an approach that uses “embedded_schemas” would look like.

Am I just being a bit dim with all this? Given the things I’ve worked on previously, I don’t think I should be struggling with this the way that I am. However, the fact is that I just can’t get my head around it, and almost would prefer to just write plain SQL. That would of course leave me without Ecto’s brilliant integration with Phoenix and assistance with storing & querying data, but at this stage, I don’t know what else I can do/read to understand Ecto properly.

Does anyone know of an example, or a book, or a video course that might help me understand this?

Cheers, Jamie.

3 Likes

phoenix_html does work completely without ecto (the integration of ecto comes with the phoenix_ecto package) so you can always set the method you need manually:

from_for form_data, action, [method: :put], fn f -> […] end

3 Likes

Brilliant thank you! I knew there would be a way of doing this, but was trying to tackle it from the Ecto side by forcibly modifying the changesets state or action.

Changing that lets me make the update to the embedded_schema through the form, and some of the subsequent data does indeed update, but certain models/tables don’t.

I think I’ve tied myself up in knots and am fighting Ecto the entire way. Perhaps I should go back to the drawing board and work out what I’m trying to accomplish, and whether I’m making things more complex than they need to be?

2 Likes

Hi @jdumont,

I am kind of having the same issue here. I am building a recipe management app which support recipe versioning. The thing is that my recipe_version schema is immutable, so when a user edits a recipe, it is implicit that he edits the last recipe version but submitting the form will actually create a new version. All the logic (inside my Kitchen context!) is working perfectly.

My data model is as simple as:
Recipe has many RecipeVersion has many {Ingredients, Steps}

I am trying to build a form where the user wouldn’t see the RecipeVersion at all. He would simply edit the Recipe name, the ingredients and steps in place.

I have created an embedded_schema that represents the form parameter but I am running into the same confusion as you.

I was wondering if you found something relevant in your case? Did you simply choose to use the [method: :put] on the form_for tag?

More generally, what are the best practices when you want to decouple your data model from your UI?

Thanks in advance!

1 Like

@hourliert setting method: :put is completely fine but you can also do this: Ecto.put_meta(embedded_schema, state: :loaded) before wrapping the schema in a changeset and therefore Phoenix<->Ecto will consider it as a PUT. That may be semantically better.

But generally speaking, decoupling your forms (UI representation) from your DB representation is a very good practice. So you are going in the right direction. I also recommend then What’s new in Ecto 2 ebook. It covers this and a couple of other things. The ebook is for 2.0 but almost all of it works just fine on 3.0.

That is awesome!! Thanks for the prompt answer. I was exactly looking for something like this Ecto.put_meta(embedded_schema, state: :loaded). I tried to create and update the __meta__ field on my embedded schema myself in the first place :sweat_smile: (totally new to Elixir and Phoenix).

I will have a look at the ebook you linked.
Thank you very much again!!!

1 Like

Hey @josevalim,
I tried your suggestion and I was expecting put_meta to create the meta field on the embedded schema since according to the doc: (https://hexdocs.pm/ecto/Ecto.Schema.html#embedded_schema/1) embedded_schemas don’t have a meta field, but it didn’t seem to work.

Also looking at the code source of Ecto: https://github.com/elixir-ecto/ecto/blob/master/lib/ecto.ex#L560, put_meta won’t match if I am calling it with an embedded_schema.

Error:

** (FunctionClauseError) no function clause matching in Ecto.put_meta/2

    The following arguments were given to Ecto.put_meta/2:

        # 1
        %MyRecipeBox.Kitchen.RecipeCreation{
          cost: nil,
          difficulty: nil,
          id: nil,
          ingredients: [],
          name: nil,
          steps: []
        }

        # 2
        [state: :loaded]

    Attempted function clauses (showing 1 out of 1):

        def put_meta(%{__meta__: meta} = struct, opts)

    (ecto) lib/ecto.ex:560: Ecto.put_meta/2

Did I miss something?

Thanks in advance :pray:

Oh, yes, I forgot that embedded do not have a meta field. So yes. In this case, you either need to make it not embdded or pass the method. Sorry for wrong advice!

Oh noo. This solution looked elegant! :joy:
In the case I am making this schema not embedded, that would mean that I HAVE to persist it somewhere? I would be back to my initial problem: coupling the database model to the UI right?

I am gonna pass the method to the form for now. If I found something more satisfying, I’ll update this post (or write it somewhere else).

Thanks a lot for the help. Really appreciate it.

No, you don’t have to. You can completely work with them forgetting completely that Ecto.Repo exists.

1 Like

Oh nice, I think I was confused by the doc.

It makes total sense. Once again, big thanks!

Slight tangent, but I think that much of the material online (both docs and tutorials) suggests that Phoenix forms is the “right” way. Of course you can structure your forms however you wish, but I think coupling is implicitly encouraged. You certainly give up a lot of wonderful developer ux when you decouple them from the DB, and it adds friction to the correct choice. I certainly doubted myself whenever I strayed from them in the early days.

Could this be an area like Phoenix pre-Contexts, where the encouraged behaviour is actually something on an anti-pattern? Perhaps much like the generators, Phoenix Forms need a disclaimer: “These are great for simple use care X, but not so great for more complicated use case Y.”

Phoenix forms are certainly the right way. Also using ecto with phoenix forms is not an issue. But for any non trivial form it’s often better (and imho easier) to have a schema/changeset or schemaless changeset special to your form’s needs and not to use the db schema directly. Or the usual example of user registration: You might have one form in the frontend, but create an user login and a profile in the backend. It’s a really nice pattern to decouple that and the only thing I not really like about it right now it consolidating the errors of the db schemas, so they’re shown in the form again. All the field mapping can become tricky.

1 Like

Agreed, it was this transition from nested forms that I always struggled to make.

This. Getting nice errors back out to the form is a perfect example of where the developer suddenly becomes more complex.

Another tangent: It’s also interesting to see what I was struggling with this time last year.

2 Likes