Search code examples
node.jspostgresqltypeorm

How to correctly seed the entities with on-to-many/many-to-one relations with Node.js + TypeORM + Postgres?


Although the TypeORM official documentation gives the answer to this question, looks like there are some pitfalls. In my case, there are not errors, but looks like the binding has not been executed correctly.

Well, I can't do exactly as in the TypeORM documentation in the code for production, so I have collected the data for diagnostic.

In my case, the BlogPost entity depends on BlogPostCategory:

import { Entity, Column, PrimaryGeneratedColumn, ManyToOne as ManyToOne, Relation } from "typeorm";
import { isUndefined } from "@yamato-daiwa/es-extensions";


@Entity()
export default class BlogPost {

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

  @Column({ type: "varchar", nullable: false })
  public readonly heading!: string;

  @Column({ type: "text", nullable: false })
  public readonly HTML!: string;

  /* [ Theory ] About `Relation` https://stackoverflow.com/a/71983552/4818123 */
  @ManyToOne(
    (): typeof BlogPostCategory => BlogPostCategory,
    (blogPostCategory: BlogPostCategory): ReadonlyArray<BlogPost> => blogPostCategory.blogPosts
  )
  public readonly category!: Relation<BlogPostCategory>;

  @Column({ type: "varchar", nullable: false })
  public readonly metaDescription!: string;

  @Column({ type: "timestamp with time zone", nullable: false })
  public readonly publishingDateTime__ISO8601!: string;

  public constructor(
    properties?: Readonly<Omit<BlogPost, "ID">>
  ) {

    if (isUndefined(properties)) {
      return;
    }


    this.heading = properties.heading;
    this.HTML = properties.HTML;
    this.category = properties.category;
    this.metaDescription = properties.metaDescription;
    this.publishingDateTime__ISO8601 = properties.publishingDateTime__ISO8601;

  }

}
@Entity()
@Unique([ "text" ])
export default class BlogPostCategory {

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

  @Column({ type: "varchar", nullable: false })
  public text!: string;

  @OneToMany(
    (): typeof BlogPost => BlogPost,
    (blogPost: BlogPost): BlogPostCategory => blogPost.category
  )
  public blogPosts!: Array<BlogPost>;


  public constructor(properties?: Readonly<Omit<BlogPostCategory, "ID">>) {

    if (isUndefined(properties)) {
      return;
    }


    this.text = properties.text;

  }

}

Although I don't know the seeding library compatible with newest version of TypeORM, in fact, the seeder is the simple script which could be run by node or ts-node, as in my case. From the viewpoint of logic, because the BlogPost depends on BlogPostCategory, it is better to create the instances of BlogPostCategory first.

import { getArrayElementSatisfiesThePredicateIfSuchElementIsExactlyOne } from "@yamato-daiwa/es-extensions";
import { DataSource } from "typeorm";

const dataSource: DataSource = new DataSource(/* ... */);

await dataSource.initialize();

// ...

const blogPostCategories: ReadonlyArray<BlogPostCategory> = await dataSource.manager.save(
    Object.
        values(initialBlogPostsCategories).
        map(
          (blogPostCategory): BlogPostCategory => new BlogPostCategory({ text: blogPostCategory.text })
        )
  );
  
console.log("=== CHECKPOINT 1 =========================================================================")
console.log(blogPostCategories);

const blogPosts: Array<BlogPost> = await dataSource.manager.save(
    initialBlogPosts.map(

        (blogPost: Readonly<Omit<BlogPost, "ID">>): BlogPost =>

            new BlogPost({
              ...blogPost,
              category: getArrayElementSatisfiesThePredicateIfSuchElementIsExactlyOne(
                blogPostCategories,
                (blogPostCategory: BlogPostCategory): boolean =>
                      blogPostCategory.text === blogPost.category.text,
                { mustThrowErrorIfElementNotFoundOrMatchesAreMultiple: true }
              )
            })

    )
  );

console.log("=== CHECKPOINT 2 =========================================================================")
console.log(blogPosts)

The example of output at CHECKPOINT 1 :

[                                              
  BlogPostCategory {                      
    ID: '1c23cf90-f112-442f-9c12-240d6749d26d',
    text: 'Category 1',                            
    blogPosts: undefined                       
  },                                           
  BlogPostCategory {                      
    ID: 'f433eeb3-22e2-4218-81ed-d3ff21fe4936',
    text: 'Category 2',                          
    blogPosts: undefined                       
  }
]

blogPosts are still undefined because they has not been created yet. I suppose, it is fine for now.

The example of output at CHECKPOINT 2 :

[
  BlogPost {
    ID: 'e0403e10-4659-4363-88e7-8e7529823b31',
    heading: 'Post 1',
    HTML: "<h1>Post 1</h1>",
    category: BlogPostCategory {
      ID: 'ae525873-91dc-4b84-92cc-ea31dcd326cb',
      text: 'Category 1',
      blogPosts: undefined
    },
    metaDescription: 'Post 1',
    publishingDateTime__ISO8601: '2024-01-25T07:23:59.840Z'
  },
  BlogPost {
    ID: '5f7dd7de-82c6-4119-8653-288d7e20a79f',
    heading: 'Post 2',
    HTML: "<h2>Post 2</h2>",
    category: BlogPostCategory {
      ID: '1c23cf90-f112-442f-9c12-240d6749d26d',
      text: 'Category 2',
      blogPosts: undefined
    },
    metaDescription: 'Post 2',
    publishingDateTime__ISO8601: '2023-12-31T15:00:00.000Z'
  }
]

We have the recursion: BlogPost has BlogPostCateogry inside, and BlogPostCateogry has BlogPosts. I am not sure about it is fine, but when I try to retrieve the blog posts from the database, they have empty category. I suppose, the problems begins here.

The database content:

enter image description here

enter image description here

I suppose the usage of constructors in the entities is not the cause, because according to TypeORM documentation, the usage of constructors is fine if the constructor parameters are optional.


Solution

  • Hi Takeshi as per the documentation of TypeORM (Check Documentation), there will be a constraint created on one of the tables (BlogPost) and to load data along with the relations we need to pass extra options to the find query.

    Like this

    const blogCategoriesWithBlogs = await dataSource.manager.find(BlogPostCategory, {
    relations: {
      blogPosts: true,
    }});
    

    With the above query, we can get the blog posts associated with that category. Hope this helps.