TypeORM ve TypeGraphQL ile GraphQL API’de ManyToMany İlişki
- GraphQL
- TypeORM
- TypeScript
- PostgreSQL
İçindekiler
TypeORM ve TypeGraphQL nedir?
TypeORM, TypeScript ve JavaScript ile kullanılabilen bir ORM’dir. Diğer JavaScript ORM’lerinden farklı olarak hem Active Record hem de Data Mapper desenlerini destekler; Hibernate, Doctrine ve Entity Framework’ten esinlenmiştir.
TypeGraphQL, GraphQL şemasını TypeScript sınıfları ve decorator’lar üzerinden tanımlamamızı sağlar. Normalde şemayı SDL’de, veri modelini ORM sınıflarında, resolver’ları da ayrıca yazarız ve bunları senkron tutmak proje büyüdükçe zorlaşır. TypeGraphQL ile aynı sınıf hem veritabanı tablosunu hem de GraphQL tipini temsil eder.
Örnek: kullanıcılar yazarları beğenir
Elimizde User ve Author var. Bir kullanıcı birden fazla yazarı beğenebilir, bir yazarın birden fazla beğeneni olabilir; ilişki iki yönden de sorgulanabilmeli. Bunun için userId ve authorId tutan user_author_like adında bir ara tablo gerekir.
user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToMany, JoinTable } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { Author } from '../author/author.entity';
@Entity()
@ObjectType()
export class User extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
@Field(() => ID)
id: string;
@Column({ unique: true })
@Field()
userName: string;
// Parola GraphQL'e açılmaz: @Field yok.
@Column()
password: string;
@ManyToMany(() => Author, (author) => author.yazariBegenenler)
@JoinTable({
name: 'user_author_like',
joinColumn: { name: 'userId', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'authorId', referencedColumnName: 'id' },
})
begenilenYazarlar: Author[];
}author.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToMany } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { User } from '../user/user.entity';
@Entity()
@ObjectType()
export class Author extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
@Field(() => ID)
id: string;
@Column()
@Field()
title: string;
@ManyToMany(() => User, (user) => user.begenilenYazarlar)
yazariBegenenler: User[];
}@JoinTable ilişkinin sahibi olan tarafa, yani yalnızca User’a eklenir. Ara tabloyu TypeORM kendisi oluşturur ve yönetir; ayrıca bir entity yazmaya gerek yoktur.
Ara tabloda ek sütun gerekiyorsa
Beğeninin ne zaman yapıldığını (likedAt) da tutmak isterseniz @ManyToMany yetmez; ara tabloyu kendi entity’si olarak modelleyip iki @ManyToOne ile bağlarsınız. Bu durumda @JoinTable kullanılmaz. İkisini birlikte kullanmak, aynı tabloyu iki kez tanımlamak demektir ve şema senkronizasyonunda çakışır.
@Entity('user_author_like')
export class UserAuthorLike extends BaseEntity {
@PrimaryColumn('uuid')
userId: string;
@PrimaryColumn('uuid')
authorId: string;
@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user: User;
@ManyToOne(() => Author, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'authorId' })
author: Author;
@CreateDateColumn({ type: 'timestamptz' })
likedAt: Date;
}Mutasyonlar
Beğenme ve beğeniyi geri alma için ilişkiyi doğrudan güncelleyen iki mutasyon yeterli. Kullanıcı kimliğini parametre olarak almayın: herkes başkası adına beğeni yapabilir. Kimliği oturumdan, yani GraphQL context’inden alın.
import { Resolver, Mutation, Arg, Ctx, Authorized, ID } from 'type-graphql';
import { User } from './user.entity';
type Context = { userId: string };
@Resolver(() => User)
export class UserResolver {
@Authorized()
@Mutation(() => Boolean)
async yazarBegen(@Arg('authorId', () => ID) authorId: string, @Ctx() { userId }: Context): Promise<boolean> {
await User.createQueryBuilder().relation(User, 'begenilenYazarlar').of(userId).add(authorId);
return true;
}
@Authorized()
@Mutation(() => Boolean)
async yazarBegenme(@Arg('authorId', () => ID) authorId: string, @Ctx() { userId }: Context): Promise<boolean> {
await User.createQueryBuilder().relation(User, 'begenilenYazarlar').of(userId).remove(authorId);
return true;
}
}İlişkiyi sorgulamak ve N+1 tuzağı
İlişkili alanları GraphQL’e açmanın en kolay yolu, ilişkiyi lazy yapıp alanı @Field ile işaretlemektir. Ama bu, listede her kullanıcı için ayrı bir sorgu demektir: 50 kullanıcı listelenirse 51 sorgu çalışır. Bunun yerine alanı bir @FieldResolver ile çözün ve istekleri DataLoader ile tek sorguda toplayın:
@FieldResolver(() => [Author])
begenilenYazarlar(@Root() user: User, @Ctx() { loaders }: Context): Promise<Author[]> {
// Aynı istekteki tüm kullanıcılar için tek bir IN (...) sorgusu çalışır.
return loaders.likedAuthorsByUserId.load(user.id);
}Böylece GraphQL Playground’da users { begenilenYazarlar { title } } gibi bir sorgu, kullanıcı sayısından bağımsız olarak iki sorguyla çözülür.
Güncelleme notu
Bu yazının ilk hali 2020’de Medium’da yayımlandı. O sürümde ara tablo hem @JoinTable hem de ayrı bir entity ile iki kez tanımlanıyor, lazy relation’ların zorunlu olduğu söyleniyor ve kullanıcı kimliği mutasyona parametre olarak veriliyordu. Burada bu üçünü düzelttim ve N+1 sorununa DataLoader çözümünü ekledim.