Skip to main content
Version: 3.0 Beta

Polymorphic Models

🔋 ZenStack vs Prisma

Polymorphic models is a major feature that sets ZenStack apart from Prisma.

ZenStack natively supports polymorphic models. As we have seen in the Polymorphism section in the data modeling part, the ZModel language allows you to define models with Object-Oriented style inheritance. This section will describe the ORM runtime behavior of polymorphic models.

CRUD behavior​

Polymorphic models' CRUD behavior is similar to that of regular models, with two major differences:

  1. Base model entities cannot be created directly as they cannot exist without an associated concrete model entity.
  2. When querying a base model (either top-level or nested), the result will include all fields of the associated concrete model (unless fields are explicitly selected). The result's type is a discriminated union, so you can use TypeScript's type narrowing to access the concrete model's specific fields.

The ORM query API hides all the complexity of managing polymorphic models for you:

  • When creating a concrete model entity, its base entity is automatically created.
  • When querying a base entity, the ORM fetches the associated concrete entity and merges the results.
  • When deleting a base or concrete entity, the ORM automatically deletes the counterpart entity.

Samples​

The schema used in the sample involves a base model and three concrete models:

Click here to open an interactive playground.
zenstack/schema.zmodel
// This is a sample model to get you started.

datasource db {
provider = 'sqlite'
}

model User {
id Int @id @default(autoincrement())
email String @unique
contents Content[]
}

model Content {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
owner User @relation(fields: [ownerId], references: [id])
ownerId Int
viewCount Int @default(0)
type String

@@delegate(type)
}

model Post extends Content {
content String
}

model Image extends Content {
data Bytes
}

model Video extends Content {
url String
}
main.ts
import { createClient } from './db';

async function main() {
const db = await createClient();

const user = await db.user.create({ data: { email: 'u1@test.com' }});

console.log('Create a Post');
console.log(
await db.post.create({
data: { name: 'Post1', content: 'First post', ownerId: user.id }
})
);

console.log('Create a Video');
console.log(
await db.video.create({
data: { name: 'Video1', url: 'http://my/video/1', ownerId: user.id }
})
);

console.log('Fetch User with contents');
console.log(
await db.user.findFirstOrThrow({ include: { contents: true } })
);

console.log('Fetch with base Content model');
const content1 = await db.content.findFirstOrThrow();
// the return type is a discriminated union
if (content1.type === 'Post') {
console.log('Got post:', content1.name, content1.content);
} else if (content1.type === 'Video') {
console.log('Got video:', content1.name, content1.url);
}

console.log('Delete Videos');
await db.video.deleteMany();
console.log('Remaining Content entities:');
// deletion of concrete entities cascades to the base
console.log(
await db.content.findMany()
);
}

main();
Comments
Feel free to ask questions, give feedback, or report issues.

Don't Spam


You can edit/delete your comments by going directly to the discussion, clicking on the 'comments' link below