📜 ⬆️ ⬇️

Hammer on the ORM

Hi, Habr!

We in Hexlet teach people how to program, but we try to be cunning: for example, under the guise of a seemingly simple PHP course, we tell people about abstractions, recursion, first class functions, closure, convolution, and generally begin the Basics of Programming with MIT'shny SIKP a, and not with classes and molds. In this and other courses, as well as in our regular webinars we talk about functional programming, about the problems of modern approaches and about the main evil: the state. In our chat, we constantly raise major discussions in which it turns out that changing a state greatly increases the complexity of the system.

Today we publish a translation of an article in which Piotr Solnitsa , one of the creators of the popular DataMapper for Ruby, talks in a similar way about interaction with the database.
')
* * *

I have been promoting a functional approach in Ruby for a long time, and, although it involves many different methods and models, there is one idea, one fundamental idea that changes everything: immutability (immunity).

But what does this even mean in Ruby? Forbid changing any objects? It will be too slow, so - no. Immunity-oriented design means that you avoid those interfaces that can change objects. Yes, many methods in Ruby change state, but when you develop object interfaces, you can create them in such a way that the objects will not change.

The use of immutable objects was a shocking discovery for me. One of the things I realized after this discovery is why object-relational mapping (ORM) is a bad idea, and because of it, we have so much unnecessary complexity.

I have been a user of various ORMs for about 10 years, which includes ~ 2 years in the working group of the Data Mapper project and ~ 2 more years in attempts to build a new version that had to overcome all the obstacles of the Active Record model. I saw them from the outside, I saw them from the inside, and I no longer want to deal with ORM. I'm over it.

I scored on ORM.

Complexity


When creating software, we need to focus on minimizing complexity as much as possible.

Object-relational mapping is nothing more than a layer of additional complexity that exists only because we want to modify objects and store them persistently in the database.

One of the main ideas of OOP is the use of abstractions that represent concepts from the real world and make it easier to explain your code, to understand it more easily. We are stuck with this concept for a long time, so that many people are almost no longer able to see something outside of it.

But you can create your objects in such a way that it will be easy to understand what is going on and not to deal with “a user who changes his address” or “a list to which the product is added”. Instead of creating objects that represent entities like user or sequence numbers, try to think about how to model business transactions, subject-oriented processes.

Hint: Be aware that everything can be modeled as a function that simply transforms the data.

Loss of compliance


The complexity behind ORM is caused by what we call the disparity between the object and relational models. This task is incredibly complex, because there are many different database models and many different ways of representing objects.

You have two options:

  1. Agree on a one-to-one mapping between the database model and your objects.
  2. Use some complex system that will connect the representation of objects in the database with the representation of objects in memory.


And both options are terrible.

The first one is especially scary, and many people in the Ruby community know why. Because the 1: 1 mapping tightly links the application layer to the database. We use Active Record ORMs long enough to understand how much this complicates our applications.

What about the second option, known as the Data Mapper template? There is no such thing in Ruby, but there are still people trying to build it. Plus, there are already several projects that are trying to implement this template.

The reality is that we didn’t even come close to solving the problem of inconsistency between the object and relational models.

Worst of all, we will never be able to solve it. You can only improve everything whenever possible, given the limitations of a specific programming language. And in the end, people will definitely switch to writing SQL queries with their hands.

The database does not exist?


In fact, it exists. And this is one of the most powerful parts of your stack. Now think for a second about this:

We pretend that there is no database so that you can modify objects and store them persistently.



I understand, everything is not as you used to. Usually we are talking about great things like “competent abstraction, where our database becomes implementation detail” or “it will be easier to change databases” or “objects are separated from the database, so the system is easier to build” and so on.

But what really it all comes down to? "I want to change objects and keep them persistent."

What if I tell you that you can separate the logic of the domain from the details of persistence, and at the same time, avoid all the complexity that accompanies ORM and variable objects?

Two magic words: functions and data types.

User does not exist


User does not exist, Order does not exist

There is no ActiveProductShippingContainerHolyMolyManager.

There is only a client that sends a request to the server . The request contains the data that the server uses to turn it into an answer .

The closest thing we can model this interaction is a function that accepts input and returns output. In fact, these are always groups of functions that ultimately return an answer.

Once you see this, there will be no need to come up with uncomfortable abstractions, which we are setting for the illusion of comfort.

And what about the data? Data means complexity, so we hide it behind objects, right? That's just it never works like in the advertising book.

Vinegret from the data with erratic and unpredictable behavior - this is what ORM is.

But how? Why?

The confusion arises because we cannot determine exactly what types of data our application has to deal with. We are happy to pass raw input directly into the ORM layer and hope for the best. Unpredictability occurs because objects are mutable, and with mutability, difficult to predict side effects come. Side effects lead to bugs.

Imagine that you can define a data type for a user, and this type ensures that the invalid state is not possible. Imagine that you can transfer this type of data from one place to another, get an answer and not worry about side effects. You see where I lead, yes?

Using functions and data types is a simpler and more flexible approach to modeling client-server interactions than any typical object-orientedness with mutable objects.

User'a does not exist, but most likely there is SignupUser. No Order, but you will definitely encounter a PlaceOrder. And when you see classes ending in Manager, just run away.

What choice do we have?


I believe that one of the most profound misconceptions of the modern OO world is:

“I need an ORM because I use an object-oriented language”

Is it really? Not really! What you need is a way to get data from a database and a data conversion level so that you can easily convert data types of a subject area to types compatible with persistence.

This will eliminate the heaps of unnecessary abstractions that appear in typical ORMs.

You can still use objects when modeling the interaction between the client and the server, but there is no need to deal with the objects being changed.

Object-oriented languages ​​that support this will live longer.

My programming style on Ruby has changed dramatically over the past few years. Getting rid of ORM was one of the best decisions I have ever made. Therefore, I work on the Ruby Object Mapper and talk about it at conferences. Perhaps this is against the generally accepted opinion, but when something common and familiar constantly brings you, there is no reason to continue to delve into problems. I do not want to know how deep you can climb.

In reality, the functionary communities are already ahead of their OO colleagues. The best we can do is to understand which major paradigms from the world of functional programming we can take and apply to our object-oriented code in order to gain. For me, it is to abandon ORM and mutable objects.

Discard the ORM. Accept unchangeable design. It works better.

Translation: Natalia Bass

Source: https://habr.com/ru/post/271117/


All Articles