Transactions
We recommend performing database operations in transactions. These are a special type of step that are optimized for database accesses. They execute as a single database transaction.
To make a TypeScript function a transaction, annotate it with the DBOS.transaction
decorator.
Then, access the database using raw SQL or one of several supported ORMs, including Knex.js, Drizzle, TypeORM, and Prisma.
You can configure which ORM to use in your dbos-config.yaml
file.
Knex is the default.
Here are some examples:
- Knex
- Drizzle
- TypeORM
- Prisma
- Raw SQL w/ Knex
interface GreetingRecord {
name: string;
note: string;
}
export class Greetings {
//...
@DBOS.transaction()
static async insertGreeting(gr: GreetingRecord) {
await DBOS.knexClient('greetings').insert(gr);
}
@DBOS.transaction({readOnly: true})
static async getGreetings(): Promise<GreetingRecord[]> {
return await DBOS.knexClient<GreetingRecord>('greetings').select('*');
}
}
export const GreetingRecord = pgTable('greetings', {
name: text('name'),
note: text('note'),
});
function getClient() { return DBOS.drizzleClient as NodePgDatabase; }
export class Greetings {
//..
@DBOS.transaction()
static async insertGreeting(name: string, note: string) {
await getClient().insert(GreetingRecord).values({name: name, note: note});
}
@DBOS.transaction({ readOnly:true })
static async getGreetings(): Promise<{name: string | null, note: string | null}[]> {
return getClient().select().from(GreetingRecord);
}
}
@Entity('greetings') //set the name of the table to 'greetings'
export class GreetingRecord {
@PrimaryGeneratedColumn() //note: TypeORM requires at least one primary key
id!: number;
@Column()
name!: string;
@Column()
note!: string;
}
function getClient() {return DBOS.typeORMClient as EntityManager;}
@OrmEntities([GreetingRecord])
export class Greetings {
//...
@DBOS.transaction()
static async insertGreeting(name: string, note: string) {
const greeting = new GreetingRecord();
greeting.name = name;
greeting.note = note;
await getClient().save(greeting);
}
@DBOS.transaction({ readOnly:true })
static async getGreetings(): Promise<GreetingRecord[]> {
return await getClient().getRepository(GreetingRecord).find();
}
}
//Model specified in prisma/schema.prisma:
//
//model GreetingRecord {
// @@map("greetings")
// greeting_id Int @id @default(autoincrement()) //Note: Prisma requires at least one primary key
// name String
// note String
//}
// Use the generated Prisma client and GreetingRecord class
import { PrismaClient, GreetingRecord } from "@prisma/client";
function getClient() {return DBOS.prismaClient as PrismaClient;}
export class Greetings {
//...
@DBOS.transaction()
static async insertGreeting(name: string, note: string) {
await getClient().greetingRecord.create({
data: {
name: name,
note: note
},
});
}
@DBOS.transaction({ readOnly:true })
static async getGreetings(): Promise<GreetingRecord[]> {
return await getClient().greetingRecord.findMany();
}
}
interface GreetingRecord {
name: string;
note: string;
}
export class Greetings {
//...
@DBOS.transaction()
static async insertGreeting(gr: GreetingRecord) {
await ctxt.knexClient.raw('INSERT INTO greetings (name, note) VALUES (?, ?)', [gr.name, gr.note]);
}
@DBOS.transaction({readOnly: true})
static async getGreetings(): Promise<GreetingRecord[]> {
const result = await DBOS.knexClient.raw('SELECT name, note FROM greetings') as { rows: GreetingRecord[] };
return result.rows;
}
}
As shown above, we suggest decorating read-only transactions with @DBOS.transaction({readOnly: true})
for better performance.
Schema Management
We strongly recommend you manage your database schema using migrations.
Migration commands are configured in your dbos-config.yaml
file.
At migration time, DBOS runs all migration commands.
Please see these guides for details on how to configure migrations with each supported ORM: