typeorm/docs/view-entities.md
Gonzalo Rodulfo Martínez 1cb738a701
feat: index support for materialized views of PostgreSQL (#9414)
* feat: Added new indices attribute to View

* feat: Added view indices methods

Such as dropViewIndex, addViewIndices, addViewIndex

Added "View" type in some parameters of methods

* feat: Added view indices support when creating new indices and dropping old indices

* ref: Renamed "table" to "view" in log when dropping view index

* feat: changed order of schema sync operations

To create a new view index, a view has to be created first.

* feat: removed unreachable code

A view object don't have its indices when creation. The indices are added to the view through the createViewIndex method.

* feat: Added view when returning TableIndex

* feat: Added view paths as argument in getViews on log method

* feat: Created createViewIndexSql

This method reuses code from createIndexSql, but eliminates the isSpatial part, because a viewColumn doesn't support this attribute.

* fix: Added missing columns const to createViewIndexSql

* feat: Removed isSpatial attribute when returning TableIndex

* feat: Added unit tests

* fix: Dropped current index to leave unique index on indices array

There was a bug that when asserting the unique index, it would compare with the previous index, even when explicitly selecting the unique index in the indices array.

* ref: lint files

* feat: added "postgres" in enabledDrivers attribute

This is to enable only PostgreSQL for the tests

* feat: added doc for materialized view indices

* ref: lint files

* feat: Added new method to create mat. view indices

This new method goes after creating the views. Aditionally, the views are now created at the end (as it was before)

* ref: prettify files

* feat: revamped tests

Replaced previous unit tests with more significant ones
2022-12-03 19:25:50 +05:00

7.9 KiB

View Entities

What is View Entity?

View entity is a class that maps to a database view. You can create a view entity by defining a new class and mark it with @ViewEntity():

@ViewEntity() accepts following options:

  • name - view name. If not specified, then view name is generated from entity class name.
  • database - database name in selected DB server.
  • schema - schema name.
  • expression - view definition. Required parameter.
  • dependsOn - List of other views on which the current views depends. If your view uses another view in it's definition, you can add it here so that migrations are generated in the correct order.

expression can be string with properly escaped columns and tables, depend on database used (postgres in example):

@ViewEntity({
    expression: `
        SELECT "post"."id" AS "id", "post"."name" AS "name", "category"."name" AS "categoryName"
        FROM "post" "post"
        LEFT JOIN "category" "category" ON "post"."categoryId" = "category"."id"
    `
})

or an instance of QueryBuilder

@ViewEntity({
    expression: (dataSource: DataSource) => dataSource
        .createQueryBuilder()
        .select("post.id", "id")
        .addSelect("post.name", "name")
        .addSelect("category.name", "categoryName")
        .from(Post, "post")
        .leftJoin(Category, "category", "category.id = post.categoryId")
})

Note: parameter binding is not supported due to drivers limitations. Use the literal parameters instead.

@ViewEntity({
    expression: (dataSource: DataSource) => dataSource
        .createQueryBuilder()
        .select("post.id", "id")
        .addSelect("post.name", "name")
        .addSelect("category.name", "categoryName")
        .from(Post, "post")
        .leftJoin(Category, "category", "category.id = post.categoryId")
        .where("category.name = :name", { name: "Cars" })  // <-- this is wrong
        .where("category.name = 'Cars'")                   // <-- and this is right
})

Each view entity must be registered in your data source options:

import { DataSource } from "typeorm"
import { UserView } from "./entity/UserView"

const dataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test",
    entities: [UserView],
})

View Entity columns

To map data from view into the correct entity columns you must mark entity columns with @ViewColumn() decorator and specify these columns as select statement aliases.

example with string expression definition:

import { ViewEntity, ViewColumn } from "typeorm"

@ViewEntity({
    expression: `
        SELECT "post"."id" AS "id", "post"."name" AS "name", "category"."name" AS "categoryName"
        FROM "post" "post"
        LEFT JOIN "category" "category" ON "post"."categoryId" = "category"."id"
    `,
})
export class PostCategory {
    @ViewColumn()
    id: number

    @ViewColumn()
    name: string

    @ViewColumn()
    categoryName: string
}

example using QueryBuilder:

import { ViewEntity, ViewColumn } from "typeorm"

@ViewEntity({
    expression: (dataSource: DataSource) =>
        dataSource
            .createQueryBuilder()
            .select("post.id", "id")
            .addSelect("post.name", "name")
            .addSelect("category.name", "categoryName")
            .from(Post, "post")
            .leftJoin(Category, "category", "category.id = post.categoryId"),
})
export class PostCategory {
    @ViewColumn()
    id: number

    @ViewColumn()
    name: string

    @ViewColumn()
    categoryName: string
}

View Column options

View Column options define additional options for your view entity columns, similar to column options for regular entities.

You can specify view column options in @ViewColumn:

@ViewColumn({
    name: "postName",
    // ...
})
name: string;

List of available options in ViewColumnOptions:

  • name: string - Column name in the database view.
  • transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType } - Used to unmarshal properties of arbitrary type DatabaseType supported by the database into a type EntityType. Arrays of transformers are also supported and are applied in reverse order when reading. Note that because database views are read-only, transformer.to(value) will never be used.

Materialized View Indices

There's support for creation of indices for materialized views if using PostgreSQL.

@ViewEntity({
    materialized: true,
    expression: (dataSource: DataSource) =>
        dataSource
            .createQueryBuilder()
            .select("post.id", "id")
            .addSelect("post.name", "name")
            .addSelect("category.name", "categoryName")
            .from(Post, "post")
            .leftJoin(Category, "category", "category.id = post.categoryId"),
})
export class PostCategory {
    @ViewColumn()
    id: number

    @Index()
    @ViewColumn()
    name: string

    @Index("catname-idx")
    @ViewColumn()
    categoryName: string
}

However, unique is currently the only supported option for indices in materialized views. The rest of the indices options will be ignored.

@Index("name-idx", { unique: true })
@ViewColumn()
name: string

Complete example

Let create two entities and a view containing aggregated data from these entities:

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class Category {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string
}
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToOne,
    JoinColumn,
} from "typeorm"
import { Category } from "./Category"

@Entity()
export class Post {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @Column()
    categoryId: number

    @ManyToOne(() => Category)
    @JoinColumn({ name: "categoryId" })
    category: Category
}
import { ViewEntity, ViewColumn, DataSource } from "typeorm"

@ViewEntity({
    expression: (dataSource: DataSource) =>
        dataSource
            .createQueryBuilder()
            .select("post.id", "id")
            .addSelect("post.name", "name")
            .addSelect("category.name", "categoryName")
            .from(Post, "post")
            .leftJoin(Category, "category", "category.id = post.categoryId"),
})
export class PostCategory {
    @ViewColumn()
    id: number

    @ViewColumn()
    name: string

    @ViewColumn()
    categoryName: string
}

then fill these tables with data and request all data from PostCategory view:

import { Category } from "./entity/Category"
import { Post } from "./entity/Post"
import { PostCategory } from "./entity/PostCategory"

const category1 = new Category()
category1.name = "Cars"
await dataSource.manager.save(category1)

const category2 = new Category()
category2.name = "Airplanes"
await dataSource.manager.save(category2)

const post1 = new Post()
post1.name = "About BMW"
post1.categoryId = category1.id
await dataSource.manager.save(post1)

const post2 = new Post()
post2.name = "About Boeing"
post2.categoryId = category2.id
await dataSource.manager.save(post2)

const postCategories = await dataSource.manager.find(PostCategory)
const postCategory = await dataSource.manager.findOneBy(PostCategory, { id: 1 })

the result in postCategories will be:

[ PostCategory { id: 1, name: 'About BMW', categoryName: 'Cars' },
  PostCategory { id: 2, name: 'About Boeing', categoryName: 'Airplanes' } ]

and in postCategory:

PostCategory { id: 1, name: 'About BMW', categoryName: 'Cars' }