Skip to content

Comparison of Knex/Objection ORM & EdgeDB query builders by implementing & testing a task model with each setup.

Notifications You must be signed in to change notification settings

MiroslavPetrik/edgedb-vs-knex

Repository files navigation

edgedb-vs-knex

This project contains two Typescript implementations of a task system. The reference implementation is in 'traditional' knex/objection ORM. The second implementation is in a modern graph-relational EdgeDB using its awesome query builder.

The goal of this project is to practicaly test out the EdgeDB and compare it side-by-side with a traditional ORM setup.

Schema

  • gray box - abstract class
  • blue box - concrete entities
  • gray arrow - inheritance
  • pink arrow - relation

Description

At the center of our model is a Task which holds a title, dueDate and assignees. Each task can be Opened, Closed or Edited multiple times. The state of the task is tracked by the TaskAction. From the combination of the dueDate and the last action we will compute a status. The status will have one of three values:

  • Completed when the task is closed.
  • PastDue when the task is not closed and the due date is in the past.
  • InProgress when the task is not closed & the due date was not reached yet.

Next, there is TaskComment related to a user & a task. Together with TaskAction, they extend the TaskNotificationEvent, meaning that when an action or comment is added to the task, the assignees will have see a 'notification flag' when quering the task.

Finally, to 'clear the notification' user will add a TaskSeenEvent which will 'shadow' the TaskNotificationEvent -- when the last seen event is the most recent than all the notification events, the task will not have a notification.

Queries/Scenarios

Based on the description, we've implemented three queries:

Query Kind EdgeDB Knex
Task status Computed property (user agnostic) taskStatus taskStatus
Task hasNotification Computed property (user specific) hasNotificationQuery hasNotification
Task countNotifications Aggregation (user specific) countNotificationsQuery countNotifications

Comparison

Migrations/Schema

With Knex, we define database schema by a set of migrations. As the project grows, we can get the schema either by mentally aggregating the migrations or introspecting a live database with applied migrations.

EdgeDB on the other hand has a schema file which we edit, and the CLI generates the migrations for us. Other ORMs like Prisma have a schema file, which adds to developer's productivity.

Typescript model

Within the knex workspace, we model our Typesscript classes with the objection.js ORM. This requires us to write write manual relations based on the foreign keys previously defined in the migrations. This can feel prodecural, repetitive or low-level. This is called object-relational impedance mismatch - we must translate the relational database model realized as tables into the javascript language model which uses objects with properties/links to other objects.

With EdgeDB, our schema represents our model which goes beyond relations. We no longer have to deal with primary & foreign keys, or manually specifying cardinality like many-to-many. The EdgeQL has a 'higher' concept of links, which abstracts away the low-level details of relational data. To get the typescript model ready, we generate query builder & interfaces without writting any code.

Class hierarchy

Our model contains abstract type TaskNotificationEvent which has two child/concrete classes TaskAction and TaskComment. Now in our second scenario/query of computing Task.hasNotification we have to query both the TaskAction and TaskComment to check if a task has notification for a user.

Within EdgeDB, the abstract TaskNotificationEvent type is queryable out-of-the-box as the engine creates a view of the child types for us.

With Knex, we had to construct such view manually in a migration.

Selecting a computed enum property

In our first scenario to select a taskStatus we've defined an enum of 3 values.

In relational tables, we usually store only normalized data, so we rather define & place the derived enum in the application logic. The taskStatus query itself uses raw SQL, which is sometimes a drawback of ORM, that you still have to use SQL.

In EdgeDB, the schema is a blend of the data model and the application logic. We can define the enum as first class model property and get it into typescript by generating the interface. There is still a room for improvement, as the generator outputs union of literals, instead of real typescript enum:

export type TaskStatus = 'InProgress' | 'PastDue' | 'Completed'

This has drawback that we use status strings in tests. We didn't see this in our knex implementation, where we've referenced the enum keys.

The expression to compute the taskStatus expressed in EdgeDB Typescript builder is much harder to read compared to its plain EdgeQL version. The e.op calls simply clutter the expression.

Testing a multi-user scenario

Our second query, to check whether a task has a notification depends on user context, meaning two users running the query can get different result.

In knex, we just pass userId as a regular query parameter.

With EdgeDB, we take an advantage of the global variables. In our tests, we create two client instances, with different currentUserId values. Then we just run the same queries for different users. This leads to a great developer experience and code readability!

Parametrized query/insertion

In our tests suites we are creating mock data under various conditions. Inserting data requires passing arguments to our queries.

With the knex implementation, we just write regular typescript functions. The query builder has a nice graph api, to support inserting nested structures & relate them.

With EdgeDB, our queries have first class support for parameters, meaning we don't have to write any wrapper functions. Moreover the graph api is a default, as its the property of the underlying EdgeQL.

To test the knex implementation we've used date-fns library to produce variable dates for our mock data. On the other hand, with EdgeDB we didn't have such need, as the query builder supports the native date functions for durations and more.

There is still room for improvement - e.g. sending a select query as a parameter.

Reducing the Typescript code (EdgeDB only)

With EdgeDB, besides the query builder, we can also use the plain EdgeQL. In fact we've mirrored the queries directly in schema. But since we wanted to have fair comparison of typescript-only queries, we wrote the queries fully in the EdgeDB Typescript query builder.

Having queries in the schema/in the DB server permits us to share the logic between clients in different languages.

To utilize these schema queries, we've also experimented with EdgeQL scripts acting as tests.

About

Comparison of Knex/Objection ORM & EdgeDB query builders by implementing & testing a task model with each setup.

Topics

Resources

Stars

Watchers

Forks