15

Defining generated columns with PostgreSQL and TypeORM

 2 years ago
source link: https://wanago.io/2021/11/29/generated-columns-postgresql-typeorm/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Defining generated columns with PostgreSQL and TypeORM

SQL

November 29, 2021

On this blog, we’ve covered a variety of different column types. So far, we’ve performed various operations on said columns such as INSERT and UPDATE to modify the data directly. In this article, we cover generated columns that work differently.

Generated columns in PostgreSQL

We call the above columns generated because PostgreSQL automatically computes their data based on predefined expressions. However, the crucial thing is that we can’t insert any data into them directly.

Generated columns became available in PostgreSQL 12 in 2019

The SQL standard consists of two types of generated columns:

  • virtual,
  • stored.

Virtual generated columns

The idea behind virtual generated columns is that it occupies no disk storage. Therefore, it is computed on the fly when requested. A good example is returning the value with the SELECT statement. Unfortunately, PostgreSQL currently doesn’t implement virtual generated columns.

Stored generated columns

The stored generated columns occupy storage in the same way as a regular column. However, instead of computing the value every time it is requested, PostgreSQL does so only when the row is modified.

To define a stored generated column, we need to use the GENERATED ALWAYS AS followed by expression and the keyword STORED:

CREATE TABLE users (
  id serial PRIMARY KEY,
  email text UNIQUE,
  first_name text,
  last_name text,
  full_name text GENERATED ALWAYS AS (
    first_name || ' ' || last_name
  ) STORED

Above, we’ve defined the full_name column to be a combination of the first_name and last_name. It is common to refer to other columns while defining a generated column. That being said, a generated column can’t refer to another generated column.

Let’s test the above table by inserting a row:

INSERT INTO users(
  email, first_name, last_name
VALUES (
  '[email protected]', 'Marcin', 'Wanago'

Screenshot-from-2021-11-25-23-48-21.png

As we can see in the above table, PostgreSQL automatically figured out the value for the full_name column.

Types of expressions we can use

There are a few different types of expressions we can use when defining a generated column. For example, besides operating on text as in the previous example, we can also do so with numbers.

CREATE TABLE routes (
  id serial PRIMARY KEY,
  distance_in_kilometers numeric,
  distance_in_miles numeric GENERATED ALWAYS AS (
    distance_in_kilometers / 1.609344
  ) STORED

Besides simple operations, we can also use functions. The crucial thing is that they have to be immutable. An immutable function can’t modify the database and must return the same result given the same arguments.

CREATE TABLE posts (
  id serial PRIMARY KEY,
  title text,
  paragraphs text[],
  paragraphs_number numeric GENERATED ALWAYS AS (
    array_length(paragraphs, 1)
  ) STORED

An interesting example of a function that is not immutable is concat. Let’s try to use it instead of the || operator:

CREATE TABLE users (
  id serial PRIMARY KEY,
  email text UNIQUE,
  first_name text,
  last_name text,
  full_name text GENERATED ALWAYS AS (
    concat(first_name, ' ', last_name)
  ) STORED

ERROR: generation expression is not immutable

Surprisingly, concat can yield different results based on the database configuration. Unfortunately, it makes it not immutable.

SET TIME ZONE 'UTC';
-- Returns "Current time: 2021-11-25 22:08:00.041641"
SELECT CONCAT('Current time: ', NOW()::TIMESTAMP);
SET TIME ZONE 'UTC+1';
-- Returns "Current time: 2021-11-25 21:08:00.041641"
SELECT CONCAT('Current time: ', NOW()::TIMESTAMP);

If you want to know more about timezones, check out Managing date and time with PostgreSQL and TypeORM

Generated columns with TypeORM

Fortunately, TypeORM started supporting generated columns for PostgreSQL a few days ago.

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
class User {
  @PrimaryGeneratedColumn()
  public id: number;
  @Column()
  public firstName: string;
  @Column()
  public lastName: string;
  @Column({
    generatedType: 'STORED',
    asExpression: `'firstName' || ' ' || 'lastName'`
  fullName: string;

Please remember that we need to use the ' sign with the column names 'firstName'  and 'lastName' above. Otherwise, PostgreSQL would transform them to firstname and lastname.

An issue with updating entities

There is an important catch when using generated columns. Consider the following example:

import {
  Column,
  Entity,
  PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  public id: number;
  @Column()
  public title: string;
  @Column('text', { array: true })
  public paragraphs: string[];
  @Column({
    generatedType: 'STORED',
    asExpression: 'array_length(paragraphs, 1)'
  public paragraphsNumber: number;

There is a good chance that our application allows us to update posts with the PUT method. With it, we expect the users of our API to send all of the properties of the entity. Unfortunately, appending the paragraphsNumber in the body of the request will cause an error.

We can quickly solve the above issue with the class-transformer library and the @Exclude() decorator:

import { IsString, IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
import { Exclude } from 'class-transformer';
export class UpdatePostDto {
  @IsNumber()
  @IsOptional()
  id: number;
  @IsString({ each: true })
  @IsNotEmpty()
  @IsOptional()
  paragraphs: string[];
  @IsString()
  @IsNotEmpty()
  @IsOptional()
  title: string;
  @Exclude()
  public paragraphsNumber: number;

With it, the class-transformer excludes the paragraphsNumber field from the body of the request.

If you want to know more about the class-transformer library, check out API with NestJS #5. Serializing the response with interceptors

Simulating virtual generated columns with getters

While PostgreSQL does not support virtual generated columns, there is a simple way of simulating them when using TypeORM and classes. To do that, we can use a getter.

There is a good chance you are using the class-transformer library along with TypeORM. If you want to return the value along with the rest of the data, you need to use the @Expose() decorator:

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Expose } from 'class-transformer';
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  public id: number;
  @Column()
  public firstName: string;
  @Column()
  public lastName: string;
  @Expose()
  public get fullName() {
    return `${this.firstName} ${this.lastName}`;

When doing the above, the database does not store the fullName value in a column. Instead, it is computed every time on the fly when accessed. The crucial thing to keep in mind is that PostgreSQL won’t know anything about the fullName value, so we can’t use it in any SQL queries.

Summary

The generated columns can come in handy when we often do a set of operations with our data. Instead of retrieving the data and performing the calculations, we can optimize this process by operating on the data beforehand. We need to keep in mind that we do the above optimization at the cost of our INSERT and UPDATE operations.

Another considerable use case for generated columns is when we want to refactor our database. For example, we can define generated columns if we’re going to change some of our columns but keep the backward compatibility.

All of the above make the generated columns a feature that is worth knowing. Especially since TypeORM just recently started supporting it for PostgreSQL.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK