Some things I have encountered when working with Ecto in Phoenix projects.
This may sound like a lot of complaining, but I am listing things I keep running into that, on their own, don't amount to much, but together, frustrate me.
This may also expose things that I don't understand about Ecto and FP. And I ask that instead of telling me I'm wrong, you consider why I may have come to these conclusions. There's a chance others will as well.
Ultimately, the choice of how Ecto works is up to those that maintain it. Software is full of opinions. But here are the main things that keep coming up for me. And in my opinion, I find them uncomfortable.
-
It's included by default now.
- When I generate a new project, Ecto is included in my
mix.exs
file and is brought in as a dependency. A default repository is created and configured to use PG. I understand that there's a way to opt out, but runningmix phoenix.new
with no arguments does not indicate how. (it's--no-ecto
.) - Suggested solution: add help for the
mix phoenix.new
command indicating options.
- When I generate a new project, Ecto is included in my
-
The steps to use a different database adapter other than pg are unclear. I can figure it out, but I have to remember each time.
- Suggested solution:
mix phoenix.new myapp --db=sqlite
could set up the appropriate config, database adapter, etc.
- Suggested solution:
-
Somewhat of a tangent but after setup, I am instructed to
mix ecto.create
without knowing what it does. When I run it, I have to wait for everything to compile before I can see a message. The installation script runsmix deps.get
but does not runmix deps.compile
for me. ** Suggested Solution** Runmix deps.compile
when creating the app if I choose to install deps. -
mix ecto.create
uses the given configuration with apostgres
user andpostgres
password. The machines I've tried this on don't have this user or password configured.- Suggested solution when instructing how to set up Ecto, specify that these connection details should be changed.
-
mix phoenix.gen.model
generates a model nicely. But it assumes all fields are required.- Suggested solution support alternative syntax for specifying required fields, or place all fields as optional.
-
In general the recommended process for working with a record is tedious.
alias MyApp.User alias MyApp.Repo changeset = User.changeset %User{}, %{:name => "Homer"} Repo.insert changeset
That's a lot of steps. I can certainly shorten it by using Repo.insert with a model, but then my casts and validations are skipped. I want to just do
~~~ alias MyApp.User User.create(%{:name => "Homer"} ~~~
and be done. And I know I can. I think changesets are nice but it feels wrong to expose all of that to controllers. I feel strongly that changesets are a detail that shouldn't be exposed directly. And thus as I build apps I am writing code to do that.
-
mix phoenix.gen.html
creates a controller. but I have to add the route manually * suggested solution Add it for me. The argument that it's better to do things manually is undermined by including Ecto andphoenix.ecto
from the beginning. The generator already assumes a ton about what I'm doing, including how I want my controllers and HTML configured. and not adding the route for me means I'm stuck - I cannot run additional mix tasks because it won't compile.
unique_constraint
fails silently when no unique constraint exists in the database. Again, I understand why, bit it's another frustration whenvalidates_uniqueness_of
works in Rails and is emulated elsewhere. Checking for the existence of a record before attempting insertion is one extra DB hit when not necessary. But the way you do it now sends the entire record to the DB only to be rejected.select count(field) from table where field = value
is less data than sending a whole blog post to the db only to be rejected because of a duplicate value.
Additionally, now I am adding logic to both my database and my codebase. And I have seen that logic get out of sync so many times in my career.
* Suggested solution Give me a validates_uniqueness as an option. It can select count(field) from table where field == value
. Let me decide to take the hit. I understand that validations are fired inside of the changeset method rather than at the time the repo receives the changeset so you're limited.
* OR give me a way to specify it when I generate the model so it can create the constraint in the model and the database
8. changeset.valid?
returns true if unique constraints are not met. I understand why, but it's another frustration.
* suggested solution None. I can't think of anything good here other than looking at the db constraints somehow.
9. This is feedback from some end users Error messages seem to be ordered as validations first, then constraints. So if I have an error in an email field, I'm not informed that the username is taken until I fix the error with the email. The documentation makes this clear but it ends up not being as friendly.
* suggested solution None.
9. Ecto.Adapters.SQL.query
requires a Repo, a statement, and params, even if I don't need them.
* Suggested solution Define Ecto.Adapters.SQL.query/2
10. Writing queries is awkward but seems to be getting better with composable queries. But I think more abstraction should be encouraged.
* MyRepo.get_by(Post, title: "My post")
seems good, but more work than Post.get_by(title: "My post")
. Abstractions are good. Think about this - if I change the name of my app or my repository, I've to change it everywhere in the application. One of the core principles of programming is abstraction. It's not just for OOP. The more things I have to pass around in my application, the more things I have to change when there's an API change. If MyRepo.get_by
changes, or the syntax for the finders changes, it's all over my app.
And I think that's the major issue I have with Ecto. I am bleeding details about how to work with it all over my code. I have Repo calls everywhere. Anywhere I need to work with data, I need the model and the repo. We all know from working with Rails that it's a good idea to insulate yourself from the framework.
Once again, let me reiterate: I understand the design choices made. But I feel they're not for me. It's become clear that those who support the way things are with Ecto are doing so out of a belief that more verbosity is better because of flexibility. However, when I look at the rest of Phoenix and how well abstracted so much of it is, Ecto is a splash of very cold water. It feels very bolted-on. And to be honest, it is. When I started working with Phoenix, it wasn't there. It kinda shows.