Search code examples
postgresqldockernestjstypeorm

`ERROR [ExceptionsHandler] relation "XXX.XXX" does not exist` on production mode with NestJS + TypeORM + Postgres


Below Nest.js + Postgres + TypeORM setup works fine during development building mode and local environment:

import MemberModel from "@DataBase/Models/MemberModel";
import MemberTypeORM_Gateway from "@DataBase/Gateways/MemberTypeORM_Gateway";
import MemberController from "./Controllers/MemberController";
import { Module as NestJS_Module } from "@nestjs/common";
import { TypeOrmModule, TypeOrmModule as TypeORM_Module } from "@nestjs/typeorm";
import type { TypeOrmModuleOptions } from "@nestjs/typeorm";


@NestJS_Module({

  imports: [

    TypeORM_Module.forRootAsync({
      useFactory: (): TypeOrmModuleOptions => ({
        type: "postgres",
        host: "localhost",
        port: 5432,
        username: "postgres",
        password: "pass1234"
        autoLoadEntities: true,
        synchronize: true
      })
    }),

    TypeOrmModule.forFeature([ MemberModel ])

  ],

  controllers: [
    MemberController
  ],

  providers: [
    {
      provide: "MemberGateway",
      useClass: MemberTypeORM_Gateway
    }
  ]

})
export default class NestJS_ApplicationRootModule {}

Herewith, the docker-compose.yml file for the local environment is:

version: "3"

services:

  Database:

    image: postgres
    container_name: LocalDatabase
    restart: always
    ports:
      - "5432:5432"

    environment:
      - POSTGRES_PASSWORD=pass1234

    volumes:
      - DataBaseData:/var/lib/postgresql/data

volumes:
  DataBaseData:
    driver: local

The production setup has below differences:

@NestJS_Module({

  imports: [

    TypeORM_Module.forRootAsync({
      useFactory: (): TypeOrmModuleOptions => ({
        type: "postgres",
        host: "database", // instead of "localhost"
        port: 5432,
        username: "postgres",
        password: "pass1234"
        autoLoadEntities: true,
        synchronize: false // instead of "true"
      })
    }),

    TypeOrmModule.forFeature([ MemberModel ])

  ],

  // ...

})
export default class NestJS_ApplicationRootModule {}

Herewith the docker-compose.yaml is:

version: "3"

services:

  node_js:

    container_name: FrontServer
    build: .
    ports: [ "8080:8080" ]
    depends_on: [ "database" ]


  database:

    container_name: Database

    image: postgres
    ports: [ "5432:5432" ]

    environment:
      - POSTGRES_PASSWORD=pass1234

    volumes:
      - Database:/data/new.jiseikan.jp

volumes:
  Database: {}

When I try get some member entities, I get the error:

 [Nest] 1  - 12/10/2022, 4:23:03 AM   ERROR [ExceptionsHandler] relation "members.members" does not exist
 QueryFailedError: relation "members.members" does not exist
     at PostgresQueryRunner.query (/var/www/example.com/node_modules/typeorm/driver/postgres/PostgresQueryRunner.js:211:19)
     at processTicksAndRejections (node:internal/process/task_queues:96:5)
     at async SelectQueryBuilder.loadRawResults (/var/www/example.com/node_modules/typeorm/query-builder/SelectQueryBuilder.js:2028:25)
     at async SelectQueryBuilder.executeEntitiesAndRawResults (/var/www/example.com/node_modules/typeorm/query-builder/SelectQueryBuilder.js:1888:26)
     at async SelectQueryBuilder.getRawAndEntities (/var/www/example.com/node_modules/typeorm/query-builder/SelectQueryBuilder.js:638:29)
     at async SelectQueryBuilder.getMany (/var/www/example.com/node_modules/typeorm/query-builder/SelectQueryBuilder.js:704:25)
     at async B.retrieveAndSortByType (/var/www/example.com/FrontServerEntryPoint.js:1:21266)
     at async /var/www/example.com/node_modules/@nestjs/core/router/router-execution-context.js:46:28
     at async /var/www/example.com/node_modules/@nestjs/core/router/router-proxy.js:9:17

Let me repeat that it occurs during production mode only.

Of course, I have checked the similar question. One solution was set synchronize to true. However, the NestJS developers has warned do not do it at production mode.

Setting synchronize: true shouldn't be used in production - otherwise you can lose production data. NestJS Documentation

Anyway, I'll tried it and nothing changed.

Additional listings

MemberModel

The NestJS developer calls it "entity" however according the Clean Architecture, "The entities (enterprise business rules) must depends on nothing" while below class depends on TypeORM. So, the "Member" entity will be at next listing.

import Member from "@EnterpriseBusinessRules/Members/Member";
import WinLossRecords from "@EnterpriseBusinessRules/Members/WinLossRecords";

import {
  Entity as TypeORM_Entity,
  Column as DatabaseColumn,
  PrimaryGeneratedColumn
} from "typeorm";


@TypeORM_Entity({ name: MemberModel.DATA_BASE_TABLE_NAME })
export default class MemberModel implements Member {

  public static readonly DATA_BASE_TABLE_NAME: string = "members";

  private static readonly defaultWinLossRecordsValue: WinLossRecords = {
    wins: 0, knockouts: 0, losses: 0, draws: 0, totalBattles: 0
  };


  @PrimaryGeneratedColumn("uuid")
  public readonly ID!: Member.ID;

  @DatabaseColumn({
    type: "varchar",
    nullable: !Member.DisplayingName.REQUIRED,
    length: Member.DisplayingName.MAXIMAL_CHARACTERS_COUNT
  })
  public readonly displayingName!: string;

  @DatabaseColumn({
    type: "text",
    nullable: !Member.AvatarURI.REQUIRED
  })
  public readonly avatarURI!: string;

  @DatabaseColumn({
    type: "text",
    nullable: !Member.LargePhotoURI.REQUIRED
  })
  public readonly largePhotoURI!: string;

  @DatabaseColumn({
    type: "enum",
    enum: Object.values(Member.Types),
    nullable: !Member.Type.REQUIRED
  })
  public readonly type!: Member.Types;


  @DatabaseColumn({
    type: "simple-json",
    default: MemberModel.defaultWinLossRecordsValue
  })
  public readonly domesticWinLossRecords!: WinLossRecords;

  @DatabaseColumn({
    type: "simple-json",
    default: MemberModel.defaultWinLossRecordsValue
  })
  public readonly overseasWinLossRecords!: WinLossRecords;

  @DatabaseColumn({
    type: "simple-json",
    default: MemberModel.defaultWinLossRecordsValue
  })
  public readonly amateurWinLossRecords!: WinLossRecords;

  @DatabaseColumn({
    type: "simple-array",
    nullable: !Member.Ranks.REQUIRED
  })
  public readonly ranks?: Array<string>;

  @DatabaseColumn({
    type: "date",
    nullable: !Member.BirthDate__ISO8601.REQUIRED
  })
  public readonly birthDate__ISO8601?: string;

  @DatabaseColumn({
    type: "varchar",
    nullable: !Member.Birthplace.REQUIRED,
    length: Member.Birthplace.MAXIMAL_CHARACTERS_COUNT
  })
  public readonly birthplace!: string;

  @DatabaseColumn({
    type: "simple-array",
    nullable: !Member.MessageSplitToParagraphs.REQUIRED
  })
  public readonly messageSplitToParagraphs?: Array<string>;

  @DatabaseColumn({
    type: "text",
    nullable: !Member.PersonalBlogURI.REQUIRED
  })
  public readonly personalBlogURI!: string;

  @DatabaseColumn({
    type: "simple-json",
    default: {}
  })
  public readonly socialNetworkProfilesURIs!: Member.SocialNetworkProfilesURIs;
}

Member

import type WinLossRecords from "./WinLossRecords";


type Member = {

  ID: Member.ID;
  displayingName: string;
  avatarURI: string;
  largePhotoURI: string;
  type: Member.Types;

  domesticWinLossRecords: WinLossRecords;
  overseasWinLossRecords: WinLossRecords;
  amateurWinLossRecords: WinLossRecords;

  ranks?: Array<string>;
  birthDate__ISO8601?: string;
  birthplace?: string;
  messageSplitToParagraphs?: Array<string>;
  personalBlogURI?: string;
  socialNetworkProfilesURIs: Member.SocialNetworkProfilesURIs;
};


namespace Member {

  export type ID = string;
  export namespace ID {

    export const TYPE: StringConstructor = String;
    export const REQUIRED: boolean = true;
    export const MAXIMAL_CHARACTERS_COUNT: number = 1;

    export function isValid(potentialID: unknown): potentialID is ID {
      return typeof potentialID === "string" && potentialID.length >= MAXIMAL_CHARACTERS_COUNT;
    }
  }

  export namespace DisplayingName {
    export const TYPE: StringConstructor = String;
    export const REQUIRED: boolean = true;
    export const MINIMAL_CHARACTERS_COUNT: number = 1;
    export const MAXIMAL_CHARACTERS_COUNT: number = 255;
  }

  // And so on.

}


export default Member;

Update: newest conditional preset

What I don't like is the MemberModel has been set twice: at entities property and also at TypeOrmModule.forFeature. Can I safely leave just one of?

@NestJS_Module({

  imports: [

    TypeORM_Module.forRootAsync({
      useFactory: (): TypeOrmModuleOptions => ({

        type: "postgres",
        host: ConfigurationRepresentative.isLocalExecutionEnvironment ? "localhost" : "database",
        port: 5432,
        username: "postgres",
        password: "pass1234"

        autoLoadEntities: ConfigurationRepresentative.isLocalExecutionEnvironment,

        ...ConfigurationRepresentative.isLocalExecutionEnvironment ? null : { entities: [ MemberModel ] },

        synchronize: ConfigurationRepresentative.isLocalExecutionEnvironment

      })
    }),

    // Duplicate?
    TypeOrmModule.forFeature([ MemberModel ])

  ]

  // ...

})
export default class NestJS_ApplicationRootModule {}

Solution

  • I would say that your production database is not identical to your development database.

    Note that synchronize, which you define as true in development, automatically performs migrations so that your database is identical to your modeling.

    Once you disable this, it is up to the developer or DBA to manually create a migration to update the database.

    Using liquibase, you can easily compare two databases and see the difference between them. I suggest you do this on your development and production bench.