TypeORM
このページでは、Prisma ORMとTypeORMを比較します。TypeORMからPrisma ORMへの移行方法については、こちらのガイドをご覧ください。
TypeORM vs Prisma ORM
Prisma ORMとTypeORMは同様の問題を解決しますが、その動作方法は大きく異なります。
TypeORMは、テーブルをモデルクラスにマッピングする従来のORMです。これらのモデルクラスを使用してSQLマイグレーションを生成できます。モデルクラスのインスタンスは、実行時にアプリケーションへのCRUDクエリのインターフェースを提供します。
Prisma ORMは、肥大化したモデルインスタンス、ビジネスロジックとストレージロジックの混在、型安全性の欠如、または遅延ロードなどが原因で予測不能なクエリなど、従来のORMの多くの問題を軽減する新しい種類のORMです。
Prisma ORMは、Prismaスキーマを使用して、宣言的な方法でアプリケーションモデルを定義します。Prisma Migrateを使用すると、PrismaスキーマからSQLマイグレーションを生成し、データベースに対して実行できます。CRUDクエリは、Node.jsおよびTypeScript用の軽量で完全に型安全なデータベースクライアントであるPrisma Clientによって提供されます。
API設計と抽象化のレベル
TypeORMとPrisma ORMは、異なるレベルの抽象化で動作します。TypeORMはAPIでSQLをミラーリングすることに近いですが、Prisma Clientは、アプリケーション開発者の一般的なタスクを念頭に置いて慎重に設計された、より高レベルの抽象化を提供します。Prisma ORMのAPI設計は、正しいことを簡単にするという考え方に大きく依存しています。
Prisma Clientはより高レベルの抽象化で動作しますが、基盤となるデータベースのすべての機能を公開するように努めており、ユースケースで必要な場合はいつでも生のSQLにドロップダウンできます。
次のセクションでは、Prisma ORMとTypeORMのAPIが特定のシナリオでどのように異なるか、およびこれらのケースにおけるPrisma ORMのAPI設計の理論的根拠について、いくつかの例を調べます。
フィルタリング
TypeORMは主に、リストまたはレコードをフィルタリングするためにSQL演算子に依存しています。たとえば、find
メソッドなどです。一方、Prisma ORMは、より一般的な演算子セットを提供しており、直感的に使用できます。また、下記の型安全セクションで説明されているように、TypeORMは多くのシナリオでフィルタークエリの型安全性を失うことにも注意する必要があります。
TypeORMとPrisma ORMの両方のフィルタリングAPIがどのように異なるかの良い例は、string
フィルターを見ることです。TypeORMは主にSQLから直接派生したILike
演算子に基づくフィルターを提供しますが、Prisma ORMは開発者が使用できるより具体的な演算子を提供します。例:contains
、startsWith
、endsWith
。
const posts = await prisma.post.findMany({
where: {
title: 'Hello World',
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { contains: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { startsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('Hello World%'),
},
})
const posts = await prisma.post.findMany({
where: {
title: { endsWith: 'Hello World' },
},
})
const posts = await postRepository.find({
where: {
title: ILike('%Hello World'),
},
})
ページネーション
TypeORMはlimit-offsetページネーションのみを提供しますが、Prisma ORMはlimit-offsetとカーソルベースの両方専用のAPIを便利に提供します。両方のアプローチの詳細については、ドキュメントのページネーションセクションまたはAPI比較の下記をご覧ください。
リレーション
外部キーを介して接続されたレコードの操作は、SQLで非常に複雑になる可能性があります。Prisma ORMの仮想リレーションフィールドの概念により、アプリケーション開発者は関連データを直感的かつ便利に操作できます。Prisma ORMのアプローチの利点の一部は次のとおりです。
- fluent APIを介したリレーションシップのトラバース(ドキュメント)
- 接続されたレコードの更新/作成を可能にするネストされた書き込み(ドキュメント)
- 関連レコードへのフィルターの適用(ドキュメント)
- JOINを気にせずにネストされたデータの簡単かつ型安全なクエリ(ドキュメント)
- モデルとそのリレーションに基づくネストされたTypeScript型定義の作成(ドキュメント)
- リレーションフィールドを介したデータモデルでのリレーションの直感的なモデリング(ドキュメント)
- リレーションテーブル(JOIN、リンク、ピボット、またはジャンクションテーブルとも呼ばれます)の暗黙的な処理(ドキュメント)
データモデリングとマイグレーション
PrismaモデルはPrismaスキーマで定義されますが、TypeORMはクラスと実験的なTypeScriptデコレーターをモデル定義に使用します。Active Record ORMパターンを使用すると、TypeORMのアプローチは、アプリケーションの成長に伴い、保守が困難になる複雑なモデルインスタンスにつながることがよくあります。
一方、Prisma ORMは、Prismaスキーマで定義されたモデルのデータを読み書きするための、調整され、完全に型安全なAPIを公開する軽量のデータベースクライアントを生成します。Active Recordではなく、DataMapper ORMパターンに従います。
Prisma ORMのデータモデリング用DSLは、シンプルで直感的で使いやすいです。VS Codeでデータをモデリングする場合、オートコンプリート、クイックフィックス、定義へのジャンプ、および開発者の生産性を向上させるその他の利点などの機能を備えたPrisma ORMの強力なVS Code拡張機能をさらに活用できます。
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToMany,
ManyToOne,
} from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
name: string
@Column({ unique: true })
email: string
@OneToMany((type) => Post, (post) => post.author)
posts: Post[]
}
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
マイグレーションは、TypeORMとPrisma ORMで同様の方法で動作します。どちらのツールも、提供されたモデル定義に基づいてSQLファイルを生成し、データベースに対して実行するためのCLIを提供するというアプローチに従います。SQLファイルは、マイグレーションの実行前に変更できるため、カスタムデータベース操作はどちらのマイグレーションシステムでも実行できます。
型安全
TypeORMは、Node.jsエコシステムで最初にTypeScriptを完全に採用したORMの1つであり、開発者がデータベースクエリの特定のレベルの型安全性を得ることを可能にする上で素晴らしい仕事をしてきました。
ただし、TypeORMの型安全性の保証が不十分になる状況が多数あります。次のセクションでは、Prisma ORMがクエリ結果の型に対してより強力な保証を提供できるシナリオについて説明します。
フィールドの選択
このセクションでは、クエリでモデルのフィールドのサブセットを選択する場合の型安全性の違いについて説明します。
TypeORM
TypeORMは、find
メソッド(例:find
、findByIds
、findOne
、...)にselect
オプションを提供します。例:
select
を使用したfind
- モデル
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
select: ['id', 'title'],
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
返されたpublishedPosts
配列の各オブジェクトは、実行時に選択されたid
およびtitle
プロパティのみを保持しますが、TypeScriptコンパイラーはこれについて何も知りません。クエリ後にPost
エンティティで定義された他のプロパティにアクセスできます。例:
const post = publishedPosts[0]
// The TypeScript compiler has no issue with this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}
このコードは実行時にエラーになります
TypeError: Cannot read property 'length' of undefined
TypeScriptコンパイラーは、返されたオブジェクトのPost
型のみを確認しますが、これらのオブジェクトが実行時に実際に保持するフィールドについては認識していません。したがって、データベースクエリで取得されていないフィールドへのアクセスから保護することはできず、ランタイムエラーが発生します。
Prisma ORM
Prisma Clientは、同じ状況で完全な型安全性を保証し、データベースから取得されなかったフィールドへのアクセスから保護できます。
Prisma Clientクエリで同じ例を考えてみましょう
select
を使用したfindMany
- モデル
const publishedPosts = await prisma.post.findMany({
where: { published: true },
select: {
id: true,
title: true,
},
})
const post = publishedPosts[0]
// The TypeScript compiler will not allow this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
この場合、TypeScriptコンパイラーはコンパイル時に次のエラーをすでにスローします
[ERROR] 14:03:39 ⨯ Unable to compile TypeScript:
src/index.ts:36:12 - error TS2339: Property 'content' does not exist on type '{ id: number; title: string; }'.
42 if (post.content.length > 0) {
これは、Prisma Clientがクエリの戻り値の型をオンザフライで生成するためです。この場合、publishedPosts
は次のように型付けされます
const publishedPosts: {
id: number
title: string
}[]
したがって、クエリで取得されなかったモデルのプロパティに誤ってアクセスすることは不可能です。
リレーションのロード
このセクションでは、クエリでモデルのリレーションをロードする場合の型安全性の違いについて説明します。従来のORMでは、これはeager loadingと呼ばれることもあります。
TypeORM
TypeORMでは、find
メソッドに渡すことができるrelations
オプションを介して、データベースからリレーションをeager loadできます。
この例を考えてみましょう
relations
を使用したfind
- モデル
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
relations: ['author'],
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
name: string
@Column({ unique: true })
email: string
@OneToMany((type) => Post, (post) => post.author)
posts: Post[]
}
select
とは異なり、TypeORMはrelations
オプションに渡される文字列に対して、オートコンプリートも型安全性も提供しません。これは、TypeScriptコンパイラーがこれらのリレーションをクエリするときに行われたタイプミスをキャッチできないことを意味します。たとえば、次のクエリを許可します
const publishedPosts: Post[] = await postRepository.find({
where: { published: true },
// this query would lead to a runtime error because of a typo
relations: ['authors'],
})
この微妙なタイプミスは、次のランタイムエラーにつながります
UnhandledPromiseRejectionWarning: Error: Relation "authors" was not found; please check if it is correct and really exists in your entity.
Prisma ORM
Prisma ORMはこの種の間違いから保護するため、アプリケーションの実行時に発生する可能性のあるエラーのクラス全体を排除します。Prisma Clientクエリでリレーションをロードするためにinclude
を使用すると、クエリを指定するためにオートコンプリートを利用できるだけでなく、クエリの結果も適切に型付けされます
relations
を使用したfind
- モデル
const publishedPosts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int?
author User? @relation(fields: [authorId], references: [id])
}
繰り返しますが、publishedPosts
の型はオンザフライで生成され、次のようになります
const publishedPosts: (Post & {
author: User
})[]
参考までに、Prisma ClientがPrismaモデル用に生成するUser
型とPost
型は次のようになります
User
Post
// Generated by Prisma ORM
export type User = {
id: number
name: string | null
email: string
}
// Generated by Prisma ORM
export type Post = {
id: number
title: string
content: string | null
published: boolean
authorId: number | null
}
フィルタリング
このセクションでは、where
を使用してレコードのリストをフィルタリングする場合の型安全性の違いについて説明します。
TypeORM
TypeORMでは、find
メソッドにwhere
オプションを渡して、特定の基準に従って返されたレコードのリストをフィルタリングできます。これらの基準は、モデルのプロパティに関して定義できます。
演算子を使用した型安全性の喪失
この例を考えてみましょう
select
を使用したfind
- モデル
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan(0),
},
})
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number
@Column()
title: string
@Column({ nullable: true })
content: string
@Column({ nullable: true })
views: number
@Column({ default: false })
published: boolean
@ManyToOne((type) => User, (user) => user.posts)
author: User
}
このコードは正常に実行され、実行時に有効なクエリを生成します。ただし、where
オプションは、さまざまなシナリオで実際には型安全ではありません。特定の型(ILike
は文字列、MoreThan
は数値で動作)でのみ機能するFindOperator
(ILike
やMoreThan
など)を使用すると、モデルのフィールドに正しい型を提供する保証が失われます。
たとえば、MoreThan
演算子に文字列を提供できます。TypeScriptコンパイラーは不平を言わず、アプリケーションは実行時にのみ失敗します
const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
views: MoreThan('test'),
},
})
上記のコードは、TypeScriptコンパイラーがキャッチしないランタイムエラーになります
error: error: invalid input syntax for type integer: "test"
存在しないプロパティの指定
また、TypeScriptコンパイラーでは、モデルに存在しないwhere
オプションでプロパティを指定できます。これもランタイムエラーになります
const publishedPosts: Post[] = await postRepository.find({
where: {
published: true,
title: ILike('Hello World'),
viewCount: 1,
},
})
この場合も、アプリケーションは次のエラーで実行時に失敗します
EntityColumnNotFound: No entity column "viewCount" was found.
Prisma ORM
型安全性の点でTypeORMで問題となる両方のフィルタリングシナリオは、Prisma ORMによって完全に型安全な方法でカバーされています。
演算子の型安全な使用法
Prisma ORMを使用すると、TypeScriptコンパイラーはフィールドごとの演算子の正しい使用法を強制します
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
views: { gt: 0 },
},
})
上記のPrisma Clientで示されているのと同じ問題のあるクエリを指定することは許可されていません
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
views: { gt: 'test' }, // Caught by the TypeScript compiler
},
})
TypeScriptコンパイラーはこれをキャッチし、アプリのランタイムエラーから保護するために次のエラーをスローします
[ERROR] 16:13:50 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ gt: string; }' is not assignable to type 'number | IntNullableFilter'.
Type '{ gt: string; }' is not assignable to type 'IntNullableFilter'.
Types of property 'gt' are incompatible.
Type 'string' is not assignable to type 'number'.
42 views: { gt: "test" }
モデルプロパティとしてのフィルターの型安全な定義
TypeORMを使用すると、モデルのフィールドにマッピングされないwhere
オプションでプロパティを指定できます。上記の例では、viewCount
のフィルタリングは、フィールドが実際にはviews
と呼ばれているため、ランタイムエラーにつながりました。
Prisma ORMを使用すると、TypeScriptコンパイラーは、モデルに存在しないwhere
内でプロパティを参照することを許可しません
const publishedPosts = await prisma.post.findMany({
where: {
published: true,
title: { contains: 'Hello World' },
viewCount: { gt: 0 }, // Caught by the TypeScript compiler
},
})
繰り返しますが、TypeScriptコンパイラーはあなた自身のミスから保護するために次のメッセージで不平を言います
[ERROR] 16:16:16 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ published: boolean; title: { contains: string; }; viewCount: { gt: number; }; }' is not assignable to type 'PostWhereInput'.
Object literal may only specify known properties, and 'viewCount' does not exist in type 'PostWhereInput'.
42 viewCount: { gt: 0 }
新しいレコードの作成
このセクションでは、新しいレコードを作成する場合の型安全性の違いについて説明します。
TypeORM
TypeORMでは、データベースに新しいレコードを作成する主な方法が2つあります。insert
とsave
です。どちらの方法でも、開発者は必須フィールドが提供されていない場合にランタイムエラーにつながる可能性のあるデータを送信できます。
この例を考えてみましょう
save
で作成insert
で作成- モデル
const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)
const userRepository = getManager().getRepository(User)
userRepository.insert({
name: 'Alice',
})
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ nullable: true })
name: string
@Column({ unique: true })
email: string
@OneToMany((type) => Post, (post) => post.author)
posts: Post[]
}
TypeORMでレコード作成にsave
またはinsert
のどちらを使用しているかに関係なく、必須フィールドの値を指定し忘れた場合、次のランタイムエラーが発生します
QueryFailedError: null value in column "email" of relation "user" violates not-null constraint
email
フィールドはUser
エンティティで必須として定義されています(これはデータベースのNOT NULL
制約によって強制されます)。
Prisma ORM
Prisma ORMは、モデルのすべての必須フィールドの値を送信することを強制することにより、これらの種類の間違いから保護します。
たとえば、必須のemail
フィールドが欠落している新しいUser
を作成しようとすると、TypeScriptコンパイラーによってキャッチされます
create
で作成- モデル
const newUser = await prisma.user.create({
data: {
name: 'Alice',
},
})
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
}
次のコンパイル時エラーが発生します
[ERROR] 10:39:07 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2741: Property 'email' is missing in type '{ name: string; }' but required in type 'UserCreateInput'.
API比較
単一オブジェクトのフェッチ
Prisma ORM
const user = await prisma.user.findUnique({
where: {
id: 1,
},
})
TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
単一オブジェクトの選択されたスカラーのフェッチ
Prisma ORM
const user = await prisma.user.findUnique({
where: {
id: 1,
},
select: {
name: true,
},
})
TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
select: ['id', 'email'],
})
リレーションのフェッチ
Prisma ORM
- includeの使用
- Fluent API
const posts = await prisma.user.findUnique({
where: {
id: 2,
},
include: {
post: true,
},
})
const posts = await prisma.user
.findUnique({
where: {
id: 2,
},
})
.post()
注:
select
はpost
配列を含むuser
オブジェクトを返しますが、fluent APIはpost
配列のみを返します。
TypeORM
relations
の使用JOIN
の使用- eager relationsの使用
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
relations: ['posts'],
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
join: {
alias: 'user',
leftJoinAndSelect: {
posts: 'user.posts',
},
},
})
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)
具体的な値のフィルタリング
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: {
contains: 'Hello',
},
},
})
TypeORM
const userRepository = getRepository(User)
const users = await userRepository.find({
where: {
name: 'Alice',
},
})
その他のフィルター条件
Prisma ORM
Prisma ORMは、最新のアプリケーション開発で一般的に使用される多くの追加フィルターを生成します。
TypeORM
TypeORMは、より複雑な比較を作成するために使用できる組み込み演算子を提供します
リレーションフィルター
Prisma ORM
Prisma ORMを使用すると、取得されるリストのモデルだけでなく、そのモデルのリレーションにも適用される基準に基づいてリストをフィルタリングできます。
たとえば、次のクエリは、タイトルに「Hello」を含む1つ以上の投稿があるユーザーを返します
const posts = await prisma.user.findMany({
where: {
Post: {
some: {
title: {
contains: 'Hello',
},
},
},
},
})
TypeORM
TypeORMは、リレーションフィルター専用のAPIを提供していません。QueryBuilder
を使用するか、クエリを手書きすることで同様の機能を取得できます。
ページネーション
Prisma ORM
カーソルスタイルのページネーション
const page = await prisma.post.findMany({
before: {
id: 242,
},
last: 20,
})
オフセットページネーション
const cc = await prisma.post.findMany({
skip: 200,
first: 20,
})
TypeORM
const postRepository = getRepository(Post)
const posts = await postRepository.find({
skip: 5,
take: 10,
})
オブジェクトの作成
Prisma ORM
const user = await prisma.user.create({
data: {
email: 'alice@prisma.io',
},
})
TypeORM
save
の使用create
の使用insert
の使用
const user = new User()
user.name = 'Alice'
user.email = 'alice@prisma.io'
await user.save()
const userRepository = getRepository(User)
const user = await userRepository.create({
name: 'Alice',
email: 'alice@prisma.io',
})
await user.save()
const userRepository = getRepository(User)
await userRepository.insert({
name: 'Alice',
email: 'alice@prisma.io',
})
オブジェクトの更新
Prisma ORM
const user = await prisma.user.update({
data: {
name: 'Alicia',
},
where: {
id: 2,
},
})
TypeORM
const userRepository = getRepository(User)
const updatedUser = await userRepository.update(id, {
name: 'James',
email: 'james@prisma.io',
})
オブジェクトの削除
Prisma ORM
const deletedUser = await prisma.user.delete({
where: {
id: 10,
},
})
TypeORM
delete
の使用remove
の使用
const userRepository = getRepository(User)
await userRepository.delete(id)
const userRepository = getRepository(User)
const deletedUser = await userRepository.remove(user)
バッチ更新
Prisma ORM
const user = await prisma.user.updateMany({
data: {
name: 'Published author!',
},
where: {
Post: {
some: {
published: true,
},
},
},
})
TypeORM
データベース内のエンティティを更新するには、クエリビルダーを使用できます。
バッチ削除
Prisma ORM
const users = await prisma.user.deleteMany({
where: {
id: {
in: [1, 2, 6, 6, 22, 21, 25],
},
},
})
TypeORM
delete
の使用remove
の使用
const userRepository = getRepository(User)
await userRepository.delete([id1, id2, id3])
const userRepository = getRepository(User)
const deleteUsers = await userRepository.remove([user1, user2, user3])
トランザクション
Prisma ORM
const user = await prisma.user.create({
data: {
email: 'bob.rufus@prisma.io',
name: 'Bob Rufus',
Post: {
create: [
{ title: 'Working at Prisma' },
{ title: 'All about databases' },
],
},
},
})
TypeORM
await getConnection().$transaction(async (transactionalEntityManager) => {
const user = getRepository(User).create({
name: 'Bob',
email: 'bob@prisma.io',
})
const post1 = getRepository(Post).create({
title: 'Join us for GraphQL Conf in 2019',
})
const post2 = getRepository(Post).create({
title: 'Subscribe to GraphQL Weekly for GraphQL news',
})
user.posts = [post1, post2]
await transactionalEntityManager.save(post1)
await transactionalEntityManager.save(post2)
await transactionalEntityManager.save(user)
})
Prismaとのつながりを保つ
以下とつながってPrismaの旅を続けましょう アクティブなコミュニティ。最新情報を入手し、参加し、他の開発者と協力しましょう
- Xでフォローしてください お知らせ、ライブイベント、役立つヒント。
- Discordに参加する 質問をしたり、コミュニティと話したり、会話を通じて積極的なサポートを受けたりできます。
- YouTubeでチャンネル登録 チュートリアル、デモ、ストリーム。
- GitHubでエンゲージ リポジトリにスターを付けたり、問題を報告したり、問題に貢献したりします。