vendure-entity-writing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVendure Entity Writing
Vendure 实体编写指南
Purpose
目的
Guide creation of Vendure database entities following TypeORM and Vendure patterns.
指导如何遵循TypeORM和Vendure的规范创建Vendure数据库实体。
When NOT to Use
不适用场景
- Plugin structure only (use vendure-plugin-writing)
- GraphQL schema only (use vendure-graphql-writing)
- Reviewing entities (use vendure-entity-reviewing)
- 仅涉及插件结构时(请使用vendure-plugin-writing)
- 仅涉及GraphQL schema时(请使用vendure-graphql-writing)
- 审核实体时(请使用vendure-entity-reviewing)
FORBIDDEN Patterns
禁止使用的模式
- Not extending VendureEntity
- Missing @Entity() decorator
- No migration file created
- Using type on columns
any - Missing indexes on foreign keys
- Direct repository access (use TransactionalConnection)
- Storing dates as strings (use Date type)
- 未继承VendureEntity
- 缺少@Entity()装饰器
- 未创建迁移文件
- 列类型使用
any - 外键未添加索引
- 直接访问仓库(应使用TransactionalConnection)
- 将日期存储为字符串(应使用Date类型)
REQUIRED Patterns
必须遵循的模式
- extends VendureEntity
- @Entity() decorator
- DeepPartial<T> constructor input
- @Column() with proper types
- Migration file for schema changes
- TransactionalConnection for queries
- Proper relation decorators
- 继承VendureEntity
- 使用@Entity()装饰器
- 构造函数参数使用DeepPartial<T>类型
- @Column()使用正确的类型
- schema变更时创建迁移文件
- 查询使用TransactionalConnection
- 使用正确的关联装饰器
Workflow
操作流程
Step 1: Create Entity Class
步骤1:创建实体类
typescript
// my-entity.entity.ts
import { DeepPartial, VendureEntity } from "@vendure/core";
import { Column, Entity, Index, ManyToOne, OneToMany } from "typeorm";
@Entity()
export class MyEntity extends VendureEntity {
constructor(input?: DeepPartial<MyEntity>) {
super(input);
}
@Column()
name: string;
@Column({ type: "text", nullable: true })
description: string | null;
@Column({ type: "int", default: 0 })
sortOrder: number;
@Column({ type: "boolean", default: true })
isActive: boolean;
@Column({ type: "timestamp", nullable: true })
publishedAt: Date | null;
}typescript
// my-entity.entity.ts
import { DeepPartial, VendureEntity } from "@vendure/core";
import { Column, Entity, Index, ManyToOne, OneToMany } from "typeorm";
@Entity()
export class MyEntity extends VendureEntity {
constructor(input?: DeepPartial<MyEntity>) {
super(input);
}
@Column()
name: string;
@Column({ type: "text", nullable: true })
description: string | null;
@Column({ type: "int", default: 0 })
sortOrder: number;
@Column({ type: "boolean", default: true })
isActive: boolean;
@Column({ type: "timestamp", nullable: true })
publishedAt: Date | null;
}Step 2: Add Relations
步骤2:添加关联关系
typescript
import { Product, Channel } from "@vendure/core";
@Entity()
export class MyEntity extends VendureEntity {
constructor(input?: DeepPartial<MyEntity>) {
super(input);
}
// Many-to-One (this entity belongs to one Product)
@Index()
@ManyToOne(() => Product, { onDelete: "CASCADE" })
product: Product;
// Store the foreign key explicitly (optional but recommended)
@Column()
productId: number;
// One-to-Many (this entity has many children)
@OneToMany(() => ChildEntity, (child) => child.parent)
children: ChildEntity[];
// Many-to-Many with join table
@ManyToMany(() => Channel)
@JoinTable()
channels: Channel[];
}typescript
import { Product, Channel } from "@vendure/core";
@Entity()
export class MyEntity extends VendureEntity {
constructor(input?: DeepPartial<MyEntity>) {
super(input);
}
// 多对一(当前实体属于一个Product)
@Index()
@ManyToOne(() => Product, { onDelete: "CASCADE" })
product: Product;
// 显式存储外键(可选但推荐)
@Column()
productId: number;
// 一对多(当前实体拥有多个子实体)
@OneToMany(() => ChildEntity, (child) => child.parent)
children: ChildEntity[];
// 多对多关联,使用中间表
@ManyToMany(() => Channel)
@JoinTable()
channels: Channel[];
}Step 3: Register in Plugin
步骤3:在插件中注册实体
typescript
@VendurePlugin({
imports: [PluginCommonModule],
entities: [MyEntity, ChildEntity],
})
export class MyPlugin {}typescript
@VendurePlugin({
imports: [PluginCommonModule],
entities: [MyEntity, ChildEntity],
})
export class MyPlugin {}Step 4: Generate Migration
步骤4:生成迁移文件
bash
undefinedbash
undefinedGenerate migration from entity changes
根据实体变更生成迁移文件
npm run migration:generate -- --name=AddMyEntity
npm run migration:generate -- --name=AddMyEntity
Or with TypeORM CLI
或使用TypeORM CLI
npx typeorm migration:generate -d ./src/migrations -n AddMyEntity
undefinednpx typeorm migration:generate -d ./src/migrations -n AddMyEntity
undefinedStep 5: Create Service for Entity
步骤5:创建实体服务类
typescript
// my-entity.service.ts
import { Injectable } from "@nestjs/common";
import { TransactionalConnection, RequestContext, ID } from "@vendure/core";
import { MyEntity } from "./my-entity.entity";
@Injectable()
export class MyEntityService {
constructor(private connection: TransactionalConnection) {}
async findAll(ctx: RequestContext): Promise<MyEntity[]> {
return this.connection.getRepository(ctx, MyEntity).find();
}
async findOne(ctx: RequestContext, id: ID): Promise<MyEntity | null> {
return this.connection.getRepository(ctx, MyEntity).findOne({
where: { id },
});
}
async findWithRelations(
ctx: RequestContext,
id: ID,
): Promise<MyEntity | null> {
return this.connection.getRepository(ctx, MyEntity).findOne({
where: { id },
relations: ["product", "children"],
});
}
async create(ctx: RequestContext, input: CreateInput): Promise<MyEntity> {
const entity = new MyEntity(input);
return this.connection.getRepository(ctx, MyEntity).save(entity);
}
async update(
ctx: RequestContext,
id: ID,
input: UpdateInput,
): Promise<MyEntity> {
const entity = await this.findOne(ctx, id);
if (!entity) {
throw new Error(`Entity with id ${id} not found`);
}
// Handle InputMaybe - check both undefined AND null
if (input.name !== undefined && input.name !== null) {
entity.name = input.name;
}
if (input.description !== undefined) {
entity.description = input.description; // null is valid for nullable
}
return this.connection.getRepository(ctx, MyEntity).save(entity);
}
async delete(ctx: RequestContext, id: ID): Promise<boolean> {
const result = await this.connection
.getRepository(ctx, MyEntity)
.delete(id);
return result.affected ? result.affected > 0 : false;
}
}typescript
// my-entity.service.ts
import { Injectable } from "@nestjs/common";
import { TransactionalConnection, RequestContext, ID } from "@vendure/core";
import { MyEntity } from "./my-entity.entity";
@Injectable()
export class MyEntityService {
constructor(private connection: TransactionalConnection) {}
async findAll(ctx: RequestContext): Promise<MyEntity[]> {
return this.connection.getRepository(ctx, MyEntity).find();
}
async findOne(ctx: RequestContext, id: ID): Promise<MyEntity | null> {
return this.connection.getRepository(ctx, MyEntity).findOne({
where: { id },
});
}
async findWithRelations(
ctx: RequestContext,
id: ID,
): Promise<MyEntity | null> {
return this.connection.getRepository(ctx, MyEntity).findOne({
where: { id },
relations: ["product", "children"],
});
}
async create(ctx: RequestContext, input: CreateInput): Promise<MyEntity> {
const entity = new MyEntity(input);
return this.connection.getRepository(ctx, MyEntity).save(entity);
}
async update(
ctx: RequestContext,
id: ID,
input: UpdateInput,
): Promise<MyEntity> {
const entity = await this.findOne(ctx, id);
if (!entity) {
throw new Error(`未找到ID为${id}的实体`);
}
// 处理InputMaybe类型 - 同时检查undefined和null
if (input.name !== undefined && input.name !== null) {
entity.name = input.name;
}
if (input.description !== undefined) {
entity.description = input.description; // null对于可空字段是合法值
}
return this.connection.getRepository(ctx, MyEntity).save(entity);
}
async delete(ctx: RequestContext, id: ID): Promise<boolean> {
const result = await this.connection
.getRepository(ctx, MyEntity)
.delete(id);
return result.affected ? result.affected > 0 : false;
}
}Common Patterns
常见模式
Channel-Aware Entity
支持多渠道的实体
typescript
import { ChannelAware, VendureEntity, Channel } from "@vendure/core";
import { Entity, ManyToMany, JoinTable } from "typeorm";
@Entity()
export class MyChannelAwareEntity
extends VendureEntity
implements ChannelAware
{
constructor(input?: DeepPartial<MyChannelAwareEntity>) {
super(input);
}
@ManyToMany(() => Channel)
@JoinTable()
channels: Channel[];
}typescript
import { ChannelAware, VendureEntity, Channel } from "@vendure/core";
import { Entity, ManyToMany, JoinTable } from "typeorm";
@Entity()
export class MyChannelAwareEntity
extends VendureEntity
implements ChannelAware
{
constructor(input?: DeepPartial<MyChannelAwareEntity>) {
super(input);
}
@ManyToMany(() => Channel)
@JoinTable()
channels: Channel[];
}Soft Delete Entity
软删除实体
typescript
import { SoftDeletable, VendureEntity } from "@vendure/core";
import { Column, Entity, DeleteDateColumn } from "typeorm";
@Entity()
export class MySoftDeleteEntity extends VendureEntity implements SoftDeletable {
constructor(input?: DeepPartial<MySoftDeleteEntity>) {
super(input);
}
@DeleteDateColumn()
deletedAt: Date | null;
}typescript
import { SoftDeletable, VendureEntity } from "@vendure/core";
import { Column, Entity, DeleteDateColumn } from "typeorm";
@Entity()
export class MySoftDeleteEntity extends VendureEntity implements SoftDeletable {
constructor(input?: DeepPartial<MySoftDeleteEntity>) {
super(input);
}
@DeleteDateColumn()
deletedAt: Date | null;
}Custom Fields on Existing Entity
为已有实体添加自定义字段
typescript
// In plugin configuration
@VendurePlugin({
imports: [PluginCommonModule],
configuration: (config) => {
config.customFields.Product.push(
{
name: "myStringField",
type: "string",
label: [{ languageCode: "en", value: "My String Field" }],
},
{
name: "myIntField",
type: "int",
defaultValue: 0,
},
{
name: "myRelation",
type: "relation",
entity: MyEntity,
eager: false,
},
);
return config;
},
})
export class CustomFieldsPlugin {}typescript
// 在插件配置中
@VendurePlugin({
imports: [PluginCommonModule],
configuration: (config) => {
config.customFields.Product.push(
{
name: "myStringField",
type: "string",
label: [{ languageCode: "en", value: "My String Field" }],
},
{
name: "myIntField",
type: "int",
defaultValue: 0,
},
{
name: "myRelation",
type: "relation",
entity: MyEntity,
eager: false,
},
);
return config;
},
})
export class CustomFieldsPlugin {}Entity with JSON Column
包含JSON列的实体
typescript
@Entity()
export class MyEntityWithJson extends VendureEntity {
constructor(input?: DeepPartial<MyEntityWithJson>) {
super(input);
}
@Column({ type: "simple-json", nullable: true })
metadata: Record<string, any> | null;
@Column({ type: "simple-array", default: "" })
tags: string[];
}typescript
@Entity()
export class MyEntityWithJson extends VendureEntity {
constructor(input?: DeepPartial<MyEntityWithJson>) {
super(input);
}
@Column({ type: "simple-json", nullable: true })
metadata: Record<string, any> | null;
@Column({ type: "simple-array", default: "" })
tags: string[];
}Date Handling (UTC)
日期处理(UTC)
typescript
@Entity()
export class MyEntityWithDates extends VendureEntity {
constructor(input?: DeepPartial<MyEntityWithDates>) {
super(input);
}
// Store as timestamp (UTC)
@Column({ type: "timestamp", nullable: true })
scheduledAt: Date | null;
// For date-only comparisons, store as string
@Column({ type: "varchar", length: 10, nullable: true })
specificDate: string | null; // Format: YYYY-MM-DD
}typescript
@Entity()
export class MyEntityWithDates extends VendureEntity {
constructor(input?: DeepPartial<MyEntityWithDates>) {
super(input);
}
// 以时间戳格式存储(UTC)
@Column({ type: "timestamp", nullable: true })
scheduledAt: Date | null;
// 仅需日期比较时,存储为字符串
@Column({ type: "varchar", length: 10, nullable: true })
specificDate: string | null; // 格式:YYYY-MM-DD
}Examples
示例
Example 1: Delivery Time Block Entity
示例1:配送时间段实体
typescript
// Based on DeliveryManager plugin pattern
@Entity()
export class DeliveryTimeBlock extends VendureEntity {
constructor(input?: DeepPartial<DeliveryTimeBlock>) {
super(input);
}
@Column()
startTime: string; // "09:00"
@Column()
endTime: string; // "12:00"
@Column({ type: "int" })
fee: number; // In smallest currency unit (cents)
@Column()
currencyCode: string;
@Column({ type: "int" })
maxDeliveries: number;
@ManyToMany(() => DeliveryDay, (day) => day.timeBlocks)
deliveryDays: DeliveryDay[];
}typescript
// 基于DeliveryManager插件的实现模式
@Entity()
export class DeliveryTimeBlock extends VendureEntity {
constructor(input?: DeepPartial<DeliveryTimeBlock>) {
super(input);
}
@Column()
startTime: string; // "09:00"
@Column()
endTime: string; // "12:00"
@Column({ type: "int" })
fee: number; // 以最小货币单位存储(如分)
@Column()
currencyCode: string;
@Column({ type: "int" })
maxDeliveries: number;
@ManyToMany(() => DeliveryDay, (day) => day.timeBlocks)
deliveryDays: DeliveryDay[];
}Example 2: Entity with Computed Property
示例2:包含计算属性的实体
typescript
@Entity()
export class OrderExtension extends VendureEntity {
constructor(input?: DeepPartial<OrderExtension>) {
super(input);
}
@Index()
@ManyToOne(() => Order)
order: Order;
@Column()
orderId: number;
@Column({ type: "int", default: 0 })
deliveryAttempts: number;
// Computed property (not stored in DB)
get hasExceededAttempts(): boolean {
return this.deliveryAttempts >= 3;
}
}typescript
@Entity()
export class OrderExtension extends VendureEntity {
constructor(input?: DeepPartial<OrderExtension>) {
super(input);
}
@Index()
@ManyToOne(() => Order)
order: Order;
@Column()
orderId: number;
@Column({ type: "int", default: 0 })
deliveryAttempts: number;
// 计算属性(不存储在数据库中)
get hasExceededAttempts(): boolean {
return this.deliveryAttempts >= 3;
}
}Migration Best Practices
迁移最佳实践
typescript
// migrations/1234567890-AddMyEntity.ts
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
export class AddMyEntity1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "my_entity",
columns: [
{ name: "id", type: "int", isPrimary: true, isGenerated: true },
{
name: "createdAt",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
{
name: "updatedAt",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
{ name: "name", type: "varchar" },
{ name: "productId", type: "int" },
],
}),
);
await queryRunner.createIndex(
"my_entity",
new TableIndex({ columnNames: ["productId"] }),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("my_entity");
}
}typescript
// migrations/1234567890-AddMyEntity.ts
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
export class AddMyEntity1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "my_entity",
columns: [
{ name: "id", type: "int", isPrimary: true, isGenerated: true },
{
name: "createdAt",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
{
name: "updatedAt",
type: "timestamp",
default: "CURRENT_TIMESTAMP",
},
{ name: "name", type: "varchar" },
{ name: "productId", type: "int" },
],
}),
);
await queryRunner.createIndex(
"my_entity",
new TableIndex({ columnNames: ["productId"] }),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("my_entity");
}
}Troubleshooting
问题排查
| Problem | Cause | Solution |
|---|---|---|
| Entity not created | Not in plugin entities | Add to @VendurePlugin({ entities: [] }) |
| Column doesn't exist | Missing migration | Generate and run migration |
| Relation not loading | Missing eager or relations | Use relations: ['x'] in query |
| Type error | Wrong column type | Match TypeScript type with DB type |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 实体未创建成功 | 未添加到插件的entities数组 | 添加到@VendurePlugin({ entities: [] })中 |
| 列不存在 | 缺少迁移文件 | 生成并执行迁移文件 |
| 关联数据未加载 | 未设置eager或查询未指定relations | 在查询中使用relations: ['x']参数 |
| 类型错误 | 列类型设置错误 | 确保TypeScript类型与数据库类型匹配 |
Related Skills
相关技能
- vendure-entity-reviewing - Entity review
- vendure-plugin-writing - Plugin structure
- vendure-graphql-writing - GraphQL schema
- vendure-entity-reviewing - 实体审核
- vendure-plugin-writing - 插件结构
- vendure-graphql-writing - GraphQL 模式