I don't like libraries/frameworks versus posts, because most of the time the comparisons are asymmetric. I'm going to use code examples, but these should be seen more as a mental aid than an opinion about the compared solutions.
How I got here
For a while I have been thinking about abstractions and their value. And more specifically the abstractions that are able to read and write data storage.
The seed in my head started to grow when I realized Doctrine's ORM and Laravel's Eloquent data storage fields had a different level of abstraction. In Eloquent there is no abstraction, in Doctrine the field name is abstracted by the model.
This means when a misnamed field is used in Eloquent it will result in a database error, where Doctrine will return an ORM error.
// Eloquent Recording::select('titel')->get(); // Doctrine query builder $querybuilder->select('p.titel')->from(Recording::class, 'p')->getResult();
This led me to Doctrine's DQL, which is an alternative to the query builder. DQL is a DSL that stays very close to the SQL syntax, which is one of the most known DSLs on its own.
$entitymanager->createQuery('SELECT p.titel FROM Recording p')->getResult();
I don't like short strings in code because of the magic number trap, that is why I started to think about longer strings. And the more I saw examples of longer strings the more I got the feeling that the abstraction was clearer with strings than solutions with design patterns.
Why I think design patterns can be a problem
When we go back to the query builder and the DQL examples, the query builder can affect the code in ways that is not possible with DQL.
/** * The assumption is there are three tables; recording, crew and persons. * Where there is a relation between recording and crew and between crew and persons. **/ // Eloquent $recording = Recording::where('id', 'tt3890160')->first(); $crew = $recording->crew; $results = $crew->map(function ($crewMember) use ($recording) { return [ 'title' => $recording->title, 'director_name' => $crewMember->category === 'director' ? $crewMember->person->name : null, 'player_name' => in_array($crewMember->category, ['actor', 'actress']) ? $crewMember->person->name : null, 'characters' => in_array($crewMember->category, ['actor', 'actress']) ? $crewMember->characters : null, ]; }); // DQL SELECT r.title, CASE WHEN c.category = 'director' THEN p.name ELSE NULL END AS director_name, CASE WHEN c.category IN ('actor', 'actress') THEN p.name ELSE NULL END AS player_name, CASE WHEN c.category IN ('actor', 'actress') THEN c.characters ELSE NULL END AS characters FROM App\Entity\Recording r LEFT JOIN r.crew c LEFT JOIN c.person p WHERE t.id = :id
I think the first noticeable difference is the result of the SQL query.
With the query builder the result needs to be processed after the data fetching, while DQL does the processing during the data fetching.
A less noticeable difference is that the $crew = $recording->crew;
line triggers an extra SQL query.
Because DQL is very close to the SQL syntax it is a lot harder to create unintentional extra SQL queries.
I think design patterns are used to make it easier to reason about code, while a DSL is used to make it easier to reason about the actions that need to happen.
The more complex the actions or action chains get, the more design patterns move away from them because they want to provide the loosest coupling that is possible. Which means the mental model you make in your head becomes larger than with a DSL.
What are the problems with a DSL
The main problem with a DSL is that creating a string that can handle current and future actions is a more upfront process than writing code and iterating it depending on new situations.
When a DSL is a string it requires more cogs than design patterns, because you could look at it as a code frontend. Behind the curtains the string will be transformed to code.
Conclusion
I see two paths to go for a DSL:
- The domain actions are well understood, then go for a DSL from the start.
- The domain actions settle after a while, access if a DSL makes the code easier to understand and less error prone.
In other cases design patterns are the solution.
Top comments (5)
That's an excellent point; hidden ORM queries were the bane of the legacy product I've been replacing.
In a legacy project I had thousands of plain SQL strings dumped all over the codebase. The worst part? Every time you read one, you have to translate the logic in your head again (adm_foo = 1 AND adm_flag = 3 kind of stuff). Throwing a small abstraction layer on top - Data Mapper, Active Record, doesn’t matter - makes it at least reusable and readable. Logic stuck in strings is just bad for humans and machines, because we end up parsing it over and over instead of actually working with it.
I agree that when the database columns are cryptic, a mapper can make the code a lot more readable. But in the long run it would be better to make the column names and values more readable.
For DSLs I don't mind a string. I learned to write in a formal and informal way. So i think it is easier to use a restricted type of sentences than introducing too much symbols. When I think of a successful DSL I think of Gerkin.
The pitfall of running the risk of unknowingly introducing more queries — and thus load — on the database is indeed a real one. However ORMs like Doctrine offer a plethora of advantages as well.
Two thoughts to add some nuance to the “more queries” problem:
Engineers should properly understand the tools they are using, especially when these tools have an impact on the application in terms of performance. This is true for ORMs just as much as it is true for programming languages. An ORM is a powerful tool designed to make our lives of maintaining code and business logic easier, and it should be sufficiently understood when working with it.
Not all pieces of code need to be super efficient; almost all code can be improved. When we accept that 100% efficiency is a myth and hardly ever a real requirement, we allow ourselves to focus on what actually matters: properly structured code that promotes maintainability, scalability, and testability. In my experience that goal is easier achieved when using ORMs.
Ultimately it’s always a trade off and the experience may vary based on the language/library. But that is unrelated from the fundamental principle.
I agree that you have to understand the way a library works. The reason for the multiple queries is because the ORM solutions use lazy loading as a default. There are ways to override the default.
The point I was trying to make with the statement is that because of the behaviour of the abstraction it is easier to make mistakes.
While DQL is using the ORM models it just makes joins easier to work with, without needing to override the lazy loading behaviour. And that is one of the benefits of using a DSL over using the models to query the database.
There is a logic error in your second reason. Scalability includes performance tweaking, so stating that the code should not be super efficient goes against scaling.