Loading…

Models or types

The main problem of the old web development school is their substitution of the type concept for the model concept. I refer to the old school (instead of the reference to “dynamic typing”) because of Java. They still continue to promote the completely outrageous idea of ​​”ORM” today. But all in good time.

 

What is a Model? Declarative description of something stored in the database. In essence, its type. All SQL implementations are notorious for their ill-conceived and limited column types. The Boolean is represented as TINYINT (1) and is returned to the compiler as 1 or 0 since the driver has no idea that this value should have been a Boolean.

 

Often dates are returned as a String even if there is a native Date. JSON fields are extremely useful, while their support is not tenable. So the need to “parse from” and “format to” SQL can be taken as inevitable. Recall also the problem of SQL injection.

 

Conclusion: keep using pure SQL is really difficult. You will probably need an extra layer. Therefore, here comes our story.

 

What is wrong with the Model? First, you may have more than one database. Wait, the second ORM and key data structures described twice in a different syntax? Rather a bad situation.

 

Second, what about the frontend? You will have to describe one structure twice – for the server and for the client. Different languages. Fullstack JS? Well, then just in a different syntax.

 

A code is declarative so that types (with all the strapping) can easily occupy 30% of the total. Therefore, the upcoming Dependent Types will only increase this percentage. How long can you mask code decomposition?

Take any ORM, but the same Bookshelf.

 

let User = bookshelf.Model.extend ({

  tableName: 'users',

  posts: function () {

    return this.hasMany (Posts);

  }

})

We describe the type User in the language of “models”. Thus, we are very limited.

 

In reality, data structures look the following way:

let T = require ("tcomb")



// Primary fields (visible for all roles except "visitor")

let PublicUser = T.struct ({

  id: Id,

  role: UserRole,

  username: T.String,

  fullname: T.String,

  blocked: T.Boolean,

  rating: T.Number,

})



// Protected fields (visible to paid users and owner)

let ProtectedUser = PublicUser.extend ({

  phone: T.maybe (T.String),

  email: T.maybe (T.String),

})



// Full "model" (all database fields) (visible to owners / admins)

let User = ProtectedUser.extend ({

  balance: T.Number,

  createdOn: T.Date,

  editedOn: T.Date,

  paidUntil: T.maybe (T.Date),

})



// Form type (create / edit)

let UserForm = T.struct ({

  username: T.String,

  fullname: T.String,

  phone: T.maybe (T.String),

  email: T.maybe (T.String),

})

Could you make this on any ORM? Not at all.

 

They will allow you to have a single type, whereas there are many in reality. You can ignore this fact, “solve” it imperatively, redeclare everything in different syntaxes with different restrictions. At this stage, there is no difference. You will lose.

 

The question of types is tightly coupled with validation. Non-dependent types systems can’t express repeatPassword == password and similar edge cases. A few of them. Validation languages ​​follow type languages. What do they add? Customizable messages … If you are first-class.

 

The issue of typing is closely related to validation. Independent type systems cannot describe repeatPassword == password and similar boundary cases. They are few. Validation rules follow typing rules with amazing accuracy. And add … custom error messages. Does not pull on justification duplication of powers. And messages are easy to implement on top of types (first-class types).

 

So further you will begin to use sad libraries like JOI, repeating again. And again. Struggling with out of sync between models and validation rules. Losing them. Those who ignore reality deserve their fate.

 

What is a reality? Types. Everything has types. The composite field “address” does not differ fundamentally from the “user”. They have the same structure. They should be described in the same language.

 

-- Counting MODELS --

user       -- Model

  address  -- JSONField

    street -- StringField



street = user.address.street -- String



-- Ladies and gentlemen! Let me introduce you to our special guest JavaScript!



----------------------------------



-- Counting TYPES --

user       -- Struct

  address  -- Struct

    street -- String



street = user.address.street -- String



-- Rather boring. Does not inspire. Too simple.

 

You need types, not models.

But I can not use pure SQL ?!

 

Just don’t use ORMs. There are so many alternatives: NoSQL, DAO, query builders with custom parsing. Everything that does not require layer-specific datastructures is better. Currying and functional programming will help you, as always.

 

// SQL + Knex

let users = map (parseAs (User)) (await db ("users"). select ("*"))



// RethinkDB + RethinkDB pool

let users = conn.run (await table ("users"))

 

Avoid things that poison the mind. Some bad ideas are very sticky. OOP, REST, ORM… The subjective perception of the importance grows with time invested (spent).

 

Models are real, but they are only a fragment of reality. We need to see the bigger picture. Describe the reality of the instruments, the expressiveness of which would be enough to reflect its complexity. Instead of cramming it into a vulgar and limited ORM vision. Here is the real “discrepancy” With the inevitable consequences in the form of introduced complexity, bugs, losses and despair.

 

I could go on with technical criticism like this or this one, but this will only dilute the main idea. I used DoctrinedORM, PropelORM, SQLAlchemy, Peewee, Bookshelf, etc. I even wrote a couple of my own in Python and PHP. The final sensations were always identical.

 

I declare that this is the crime of the 2000 back-end developers. They did not have adequate NoSQL solutions. They were not aware of the alternatives. They needed to “just do their job.” Some excuses are pertinent.

 

Then they believed in their own hack. That their crutches over SQL solution flaws are useful in their own right. Instead of pressure to improve the SQL drivers (removing ORM rationales one by one), they chose a deflection. And they infected thousands of minds.

The disease is spreading. There are already ORM ODMs for Mongo. For RethinkDB. Which are nothing more than a terrible substitute for typing systems? Typing system for the poor.

 

And (at the very end) there are people transferring the “Unit Of Work” pattern to the web. Uncontrolled, implicit caching of every request in a non-interactive environment. Sociopathy. Seeing the world through the prism of “models” is obviously dangerous.

There are other points worth mentioning. JOIN concept. SQL joins stick together data, creating new obstacles to typing.

SELECT * FROM users

INNER JOIN comments

ON comments.user_id = users.id

will return a list of lines where user data and comment data will be completely mixed. Perhaps this is another problem that ORM had to solve. One more, actually, specific to SQL.

You do not want to describe a special type only for JOIN? And you have to, because your types do not cover the glued case.

And now look at how unions are implemented in RethinkDB. The resulting sequence contains documents with left and right parts, to which you can apply a zip (if you wish) or otherwise process it. You know which fields come from which table.

{

  "left": {

    "id": "543ad9c8-1744-4001-bb5e-450b2565d02c",

    "text": ""

    "userId": "064058b6-cea9-4117-b92d-c911027a725a"

  },

  "right": {

    "id": "064058b6-cea9-4117-b92d-c911027a725a",

    "fullname": "Abraham Lincoln"

  }

}

 

I can’t explain emotionally enough how much smarter this option is. How much more convenient and user-friendly. People declaring NoSQL movement “fashionable” have vision problems.

And now to the question REST. Groping the context of a software crash of two thousandths, we can in no way miss this idol.

Consider the following ways to create a new User:

POST / users {username: “jack”, …} – body should not have id (autogeneration)

PUT / users /: id {username: “jack”, …} – body must not have id (conflict)

 

The data sent to the body should not have an id. So you have to allocate one more type to this (see UserForm above).

Although technically, you can restrict yourself to User only and refuse UserForm, it will complicate things dramatically. You will have to manage all fields manually. Imperatively.

Protect from id substitution

Protect yourself from increasing balance

Protect against role substitution

And so on and so forth. Whereas with adequate typing, it is simple:

router.post ("/ api / users / role /: role",

  KoaValidate ({

    "params.role": UserRole,

    "body": UserForm,

  })

  function * (next) {

    ...

  }

)

 

Declaratively. Simply. No duplication. Now go do it on Knex, Bookshelf, Joi and on some kind of frontend validation library.

 

What if you had the simplest case when one type would be enough? The obvious way:

PUT / users {id: “1”, username: “jack”, …}

 

But this is not a breeze! Roy Fielding personally banned it. And here at IT, we are very sensitive to the opinions of the gurus. And now seriously – who has the right to dictate such details? Who is dragging things that are specific to the application, not even in the library? In the protocol! And it does it all the time.

 

Who are these people? What are they doing in the industry? With industry? But this is a topic for another series.

 

I really hope GraphQL will undermine the REST position sufficiently. With all its flaws, the Facebook team deserves respect at least for its bravery. They are trying to “destroy” the three holy grails of the Web: CSS, templates and REST. Unprecedented attempt. And I can not say that I will regret the last two.

Conclusion

So, what perspectives do we have? Should we wait until this models madness will pass someday? 

 

Yes, but we need first-class types. As far as I know, TypeScript does not give access to information about types. Technically, it could be exposed through hooks or a plugin system, but for now, this is not the case. Some people were interested in such things, but so far this has not received due attention.

 

There are bold individual attempts worthy of attention. Without hooks (and other help from the compiler), they are forced to reproduce the parser functionality. However, it (already) works and this is very cool.


Leave a Comment