Post

ecto queries in liveviews and controllers? discuss. 🍿 #ElixirLang #phoenix #code

2 reposts17 likes

Imo an applications core should provide features usable by many consumers, where web just happens to be one of. Others could be background workers, other types of interfaces like clis or other networking protocols. Having ecto queries as part of just one of such consumer most often violates that.

How has this affected your test setup? That's was my initial concern: does it change the tests in a negative way that outweighs the supposed benefits of direct queries in controllers/LV?

I guess I wonder if it introduces a slippery slope? Why just Ecto queries? Why not API calls? Or fiddling with the filesystem? Maybe it doesn't if you limit it, but that's the slope.

good follow up. i have been working with relatively few external apis, so idk to be honest. i'm thinking of ecto as the api for reading data, where i specify what i want within the lv where it's needed. much of the pain i'm responding to is ill-conceived abstractions for filtering and bespoke fns.

look, many many sins were committed in the liveview tests. would love to start over given the time and budget. practically, there should be no impact to the lv tests, though. if the lv tests focus on behaviour, it shouldn't matter whether the read queries live in the LV or a context module.

one thing i may do differently in the future is test the presence of whatever components i'm using, then make assertions on the contents of those components in separate test modules. today, most of the lv tests i've written are doing way too much. this could be a six-vs-half-dozen problem in the end

Maybe. Have a composable query module that web layer can access - chances are you will need parts of those queries elsewhere too. Context module should handle the actual fetch though so you have a spot to put weird stuff like loading virtual fields.

nothing wrong with a context module on the web side if it starts to diverge too much from the data/business side. Eg, some virtual fields only need loading to answer authz questions for rendering.

I remember hearing you talk about this approach in the past and liking the idea. I’ve struggled to find the right level of abstraction tho. Often it feels like an unnecessary layer over ecto. I can think of common filters that could work well as helpers tho. Maybe the mistake is early abstraction

The context is where the "fetch/list" happens and yeah could end up being a bit too convoluted when starting, but I think its the right place to consolidate when needed. The query module however doesn't actually _do_ anything but help compose the query with domain words-- the liveview would fetch.

I think they are totally fine. Most of these queries are driven by specific view requirements and there is no point in littering contexts with functions like get_five_recent_nonshipped_ordera_by_user. Context should be for "serious" business logic.

In your example, the problem with calling multiple context functions that not all context functions are guaranteed to be composable within a transaction. They have effects like broadcasting over pubsub, or ordering requirements etc.

If context functions were more flexible (like Ash actions), then you could have your cake and eat it too here. For example, you might have an action like `nonshipped_orders_for_user`, and the caller can apply a limit and a filter.

It depends 🤷 Mostly no. For tiny simple things, sure. As a way of growing into something more complex, fine. And maybe for a pure read-model query in a CQRS like application. In all other cases, probably not the best idea. Adding some structure will not be a bad thing. And it's often easier to test.

My thinking on this one has evolved after working on some medium to large-scale code repos. When I started writing code professionally, I had absorbed some of the best practices common in MVC frameworks, DDD, and other programming patterns. btw, thread length != rage 😜

In my first coding gigs, I experienced first-hand the impact of poorly organized code. SQL queries were scattered throughout template code, everything was mutable, and debugging was painfully tedious. I scoffed and rolled my eyes.

Later, I followed the patterns laid out by the Phoenix code generators. Where contexts contained CRUD code, controllers and LiveViews called those context functions. One of the biggest pain points that has grown out of this pattern, though, is query code - specifically *reading* data from a Repo.

While I can imagine some applications that have identical queries used across multiple call sites, this hasn't been my experience. Instead, I've seen hastily added functions in contexts which serve a single call site elsewhere in the codebase. These functions often have hilariously long names.

Shouldn’t be writing controllers at all 😅 Ecto queries shouldn’t be done in liveviews though, no. For small enough apps it doesn’t matter but in the general case you need an application layer of some kind always 🤗

I’m on the side of not exposing query generation outside of context. I usually end up with either many small functions that does the same thing with slightly different query or come up with yet another way to re-implement querying structure which always sucks. Recently I’m exploring this approach :

Once in a while I get too clever and make a context function that lets me dynamically do stuff like preload assocs, select specific fields, etc. only to realize I've rewritten a tiny subset of Ecto, on top of Ecto, for no discernable benefit. So... Sure, if it's early or very specific to a liveview

Makes sense! I'm trying to explicitly define when/why I move things to a context, which I usually do based on intuition. What are the non-ecto things we typically need to do? So far I've got: - Authorization - Compute/combine data - Extra filtering - Pagination - Caching Any others?

I'd add mutating records -- I think querying (read-only) records is fine to have more spread-about, but changing those records probably ought to funneled through a context module for consistency.

This is interesting. To me, it feels like a possible improvement over the bespoke query functions I've seen, but it still requires coordination between modules. I think I still prefer to go straight to the source via Ecto.Query.