7 ms·
I strongly agree with this view. Some things in particular that I think become anti-patterns as your Django app grows: 1. Everything is coupled to the ORM mode
by theptip 6y ago
I strongly agree with this view. Some things in particular that I think become anti-patterns as your Django app grows:
1. Everything is coupled to the ORM models / QuerySet, so any part of your stack can mutate the request's query. On one hand this is great for things like dynamic APIs and adding arbitrary filter params, but it's really hard to keep your concerns cleanly separated. (You could use Django Seal to work around the QuerySet mutability issues but it requires finesse).
2. The ActiveRecord pattern of calling model.save() to write to the DB is actually really restrictive; it forces you to conflate DB logic with your business logic, where the former often more naturally spans multiple domain models (i.e. your DB session logic should be at the application layer while your business logic is at the domain layer, in DDD terminology). The NHibernate / SQLAlchemy pattern of tracking dirty writes and only making DB requests when you call Session.flush() / Session.commit() is much more flexible. An example is if you have an object with a list of other objects, you might want to write some code in a transaction like "for day in week: parent.children.add(calculate_child_for(day))". In Django you'd be saying `parent.child_set.add(...)` which immediately runs a DB query. If your logic fails on the last child, you don't need to have sent the earlier DB requests. So instead you end up having to contort your business logic to avoid DB side-effects, collecting a list of unsaved items to later save. Using dirty-tracking means you can run your algorithm in "plan mode" / no side-effects by just omitting the "Session.commit()" at the end; no spurious DB calls will be made.
3. The ActiveRecord pattern of writing "model.fieldname = foo" and having every part of the stack treat your objects like CRUD data dictionaries completely breaks encapsulation; if you're trying to write proper domain models that encapsulate your business logic, you don't want everybody to be able to poke arbitrary state. You end up having to guard against everything being in arbitrarily bad states, because every field is public. In my experience it's much easier to test and less confusing to make most class members private, and have well-defined state-transition methods. But making your model members private in Django is obnoxiously verbose and you're swimming against the current at every step; the whole system is based on the assumption that your API is CRUD; you need to say something like `_owner_name = CharField(db_column='owner_name', verbose_name='owner name', ....)` in order to avoid borking your DB schema and admin pages.
4. Django apps are a trap. The tutorials suggest you should expect to create multiple apps but this will make your codebase a pain to refactor; you can't migrate models between apps, and so you're stuck with the first app structure you go with. Just use a single app `core` and pretend that apps are not a thing, unless you are implementing a completely standalone composable app in another repository.
5. Django is in general more on the "config over convention" end of the spectrum than, say, Rails. But it still has some annoying magic like requiring all your model files to be imported in the `appname.models` module. This means you need to do wacky re-importing in `appname/models/__init__.py` if you want to avoid having all your models in one file. Why not just allow us to configure the model path(s) like we can configure template and other paths?
Having said all this, I agree with you that Django is really productive for the early phases of a project. When I started working in Flask I had to spend a few days figuring out each of a large number of things that are just batteries-included in the Django framework. So I can see the appeal, and for small side-projects I'd still probably reach for Django. But I think I'd probably start from Flask or FastAPI for new startups.
- leetrout 6y agoI actually prefer #5 because you are explicitly “exporting” the parts Django cares about and ensures you aren’t stomping on other models. The part that stinks is dealing with cyclical imports. At the end of the day it comes down to discipline and leaning on the framework to help keep things clean is Not A Bad Thing(tm) IMO. But I am not one that feels like it robs me of my expressiveness because I’d rather have something consistent to build on across multiple projects instead of trying different patterns within the web stack that flask / fastapi let you get away with. That being said, fast api seems great especially if you don’t have to deal with the overhead of user management and such directly.
- skrtskrt 6y agoIn my experience, within an organization that runs services/microservices, having a service template is a good way to handle having a consistent approach across services instead of each team picking their own favorite libraries to flesh out a small/micro frameworks like FastAPI or Flask. Any internal libraries should be very very light wrappers, if they exist at all. Putting more into the service template or even using code generation allows things to slowly improve and change without being pinned to some old crappy code in a shared internal library, or managing breaking library changes across teams.
- theptip 6y agoI think the "Djangonic" approach would be to make this configurable, but with defaults that give you convenient magic for the simple/happy path. For example template paths are completely configurable (indeed you can plug new lookup strategies in addition to just overriding the default paths), but the defaults are "look in this well-defined location". This is more of a DX gripe than a reason not to use Django, it should probably not be in my list if I'm being fair.
- skrtskrt 6y agoThis really closely tracks my opinions/experience. Manually adding a domain/service layer over the models and never exposing the models across domain services or up to the application layer seems really tedious when you're doing it, but pays dividends down the line. The object returned from the domain layer interface is an attrs, pydantic, or just a plain Python class with no connection to the database. This encapsulates the saving-to-the-datastore logic behind the domain layer interface, so other parts of the app can't just do model.a = b; model.save() wherever and whenever they want. The domain interface decides what is allowed, and can handle & hide session logic, writing or updating related records, etc. Further, you don't have to have a bunch of @property calculated fields that include lots of business logic on the model, causing the model to bloat to a 500 line file and conflating business logic with database implementation details. Business logic can all go in the domain layer.