সরাসরি পর্বে যান
পর্ব ৩৯ · ৯৩-এর মধ্যে · মডিউল ৬

Prisma — schema, client আর CRUD

Prisma ORM 7 — Schema, Generated Client and CRUD

Taught in Bangla, this lesson sets up Prisma ORM 7 from zero with its generated client folder, driver adapters and config file, reads schema.prisma, stores money as integer paisa rather than Decimal with a real silent-rounding trap, runs prisma generate for a typed client, performs CRUD while viewing the SQL, reads with where, select, orderBy, take, skip and cursor, maps P2002 and P2025 to API errors, and keeps one client across hot reloads.

এই পর্বে যা শিখবেন

  • একটি নতুন project-এ বর্তমান stable Prisma বসানো, আর latest ট্যাগের ফাঁদ এড়ানো
  • schema.prisma-র model, ঘরের ধরন আর @id, @unique, @map, @updatedAt পড়া ও লেখা
  • CRUD, where, select, orderBy, take, skip আর cursor চালানো, আর প্রতিটির আসল SQL দেখা
  • P2002 আর P2025-কে API-র 409 আর 404-এ বদলানো
  • একটি singleton client বানিয়ে পর্ব ২৯-এর repository-তে জোড়া

১ · Prisma কী, আর আজ কোন version

পর্ব ৩৮-এ দেখেছেন, ORM মানে object আর table-এর মাঝের অনুবাদক। Prisma সেই দলের, তবে এর ধরন একটু আলাদা। এখানে class লিখে table বোঝাতে হয় না। আপনি একটি আলাদা ফাইলে দোকানের তথ্যের নকশা লেখেন, আর সেই নকশা থেকে কোড তৈরি হয়।

দর্জির মাপের খাতা ভাবুন। খাতায় একবার মাপ লেখা হলো। সেই মাপ থেকেই কাপড় কাটা হয়, আবার সেই মাপ দেখেই সহকারী জানেন কোন পকেট আছে, কোনটি নেই। schema.prisma হলো সেই খাতা। কাপড় কাটা হলো database-এর table. আর সহকারী হলো Prisma Client, যিনি খাতায় নেই এমন পকেটের কথা বললেই থামিয়ে দেন।

শুরুর আগে version দেখা দরকার, কারণ এখানে একটি আসল ফাঁদ আছে। লেখার সময়ে (নভেম্বর ২০২৬) npm-এ সত্যিকারের run:

$ npm view prisma dist-tags
{ ...
  next: '8.0.0-rc.10',
  prev: '7.10.1',
  latest: '8.0.0-rc.29',
  ... }          (ট্রিম করা)

$ npm view @prisma/client dist-tags
{ ... latest: '7.10.1', ... }

অর্থাৎ npm install prisma লিখলে আপনি একটি release candidate পাবেন, অথচ @prisma/client থাকবে 7-এ। দুটির version আলাদা হলে গোলমাল হবে। Prisma-র নিজের docs-ও এখন 7-এর জন্য prisma@prev লিখতে বলে। এই পর্ব তাই নির্দিষ্ট version-এ চলেছে: Prisma 7.10.1. আমাদের lab-এ CLI একবার update-এর একটি বার্তাও দেখিয়েছে, যাতে prisma@latest লেখা ছিল। সেই বার্তা না ভেবে মানবেন না।

এই পর্বের সব “সত্যিকারের run”: Node 24.15.0, Prisma 7.10.1, @prisma/adapter-pg 7.10.1, pg 8.23.1, TypeScript 5.9.3, tsx 4.23.15, Express 5.3.0, Zod 4.6.7. Database হলো আসল PostgreSQL, চলেছে npm-এর embedded-postgres 18.6.0-beta.19 প্যাকেজে। সেটি beta, তাই শুধু শেখার lab-এর জন্য। পর্ব ২৯-এর TypeScript 7.0.2 দিয়েও একই কোড type-check পাস করেছে, তবে Prisma-র docs 5.9.x-এর কথা বলে। পাতার ভেতরে Prisma চলে না, তাই নিজে চালানো যায় এমন block-গুলো “শেখার মডেল”। Version বদলাতে পারে, নিজে দেখে নিন।

এবার পুরো ছবিটি দেখুন। Prisma-র দুটি জগৎ আছে: একটি শুধু তৈরির সময়ে চলে, অন্যটি অ্যাপ চলার সময়ে।

Prisma 7-এর দুই জগৎ

Build time (CLI) and runtime (client, adapter, driver)

Prisma 7: তৈরির সময় আর চলার সময়ের পথ তৈরির সময় (CLI) schema.prisma prisma7.config.ts prisma CLI generated/prisma schema engine generate db push চলার সময় (আপনার অ্যাপ) আপনার কোড PrismaClient adapter-pg pg (driver, pool) PostgreSQL
ছবি ৩৯.১ — generate তৈরি করে কোড, db push তৈরি করে table. অ্যাপ চলার সময়ে CLI লাগে না।

আগের version-গুলোতে client-এর ভেতরে Rust-এ লেখা একটি আলাদা query engine চলত। Prisma 7 (নভেম্বর ২০২৫) থেকে সেটি client-এর ডিফল্ট পথ থেকে সরে গেছে। এখন query compiler SQL বানায়, আর আসল কথা বলে সাধারণ Node driver pg. তবে “Rust-free” কথাটি client-এর জন্য। আমাদের prisma -v-এ schema engine এখনো একটি আলাদা binary হিসেবেই দেখিয়েছে, যা table বদলানোর কাজ করে।

২ · শূন্য থেকে setup

একটি খালি folder-এ শুরু করি। Prisma 7-এর দরকার Node 20.19 বা তার ওপরে, TypeScript 5.4 বা তার ওপরে, আর docs অনুযায়ী ESM project. প্রথমে package.json-এ "type": "module", তারপর install:

npm pkg set type=module
npm install -D prisma@7.10.1 typescript tsx @types/node @types/pg
npm install @prisma/client@7.10.1 @prisma/adapter-pg@7.10.1 pg dotenv
npx prisma init --datasource-provider postgresql \
  --output ../generated/prisma --no-skills

সত্যিকারের run-এ init বলল (ট্রিম করা):

Initialized Prisma in your project

  prisma/
    schema.prisma
  prisma7.config.ts
  .env
  .gitignore
…
Then, define your models in prisma/schema.prisma and run prisma migrate dev to apply your schema.

একটি নাম খেয়াল করুন: prisma7.config.ts. পুরোনো লেখা আর বেশিরভাগ টিউটোরিয়ালে পাবেন prisma.config.ts. Prisma 7.10.0-এর release notes অনুযায়ী নতুন নামটি এসেছে, যাতে একই project-এ Prisma 7 আর Prisma 8 পাশাপাশি থাকতে পারে, আর Prisma 7-এর CLI দিয়ে নতুন শুরু করা project এই নামই পায়। 7.10.1-এর CLI দুটো নামই খুঁজে নেয়: আগে prisma7.config.*, না পেলে পুরোনো prisma.config.*. তাই দুটি ফাইল একসাথে রাখবেন না, নইলে পুরোনোটি চুপচাপ উপেক্ষিত হবে। ভেতরে যা তৈরি হলো (মন্তব্য বাদে হুবহু):

import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});

এখানে তিনটি জিনিস আগের চেয়ে বদলেছে, আর AI প্রায়ই পুরোনোটি লেখে:

বিষয়Prisma 7পুরোনো অভ্যাস
Database-এর URLconfig ফাইলেschema-র url = env(...), এখন error
.env পড়ানিজে: import "dotenv/config"Prisma নিজে পড়ে নিত
Generatorprisma-client, output বাধ্যতামূলকprisma-client-js
Client-এর জায়গাgenerated/prisma/client.tsnode_modules, @prisma/client থেকে import
সংযোগnew PrismaClient({ adapter })new PrismaClient()

প্রথম ঘরের error আমরা সত্যিই চালিয়ে দেখেছি। পুরোনো schema-য় prisma validate বলল:

error: The datasource property `url` is no longer supported in schema files.
Move connection URLs for Migrate to `prisma.config.ts` and pass either `adapter`
for a direct database connection or `accelerateUrl` for Accelerate to the
`PrismaClient` constructor.

এবার .env. এতে থাকে connection string, যা একটি secret. init-এর বানানো .gitignore এই ফাইল আর /generated/prisma, দুটোকেই বাদ দেয়। নিচের মানগুলো নকল:

DATABASE_URL="postgresql://bazar:example-secret@127.0.0.1:5432/bazarghar?schema=public"

/generated/prisma Git-এ না থাকলে নতুন মেশিনে বা CI-তে client থাকে না। Install-এর পরে npx prisma generate নিজে চালাতে হবে। আর dotenv-এর লাইন ভুলে গেলে কী হয়? আমাদের run-এ DATABASE_URL ছিল undefined, pg তার ডিফল্ট ঠিকানায় গেল, আর এলো ECONNREFUSED. ভুলটি সংযোগের মনে হয়, আসল কারণ env.

৩ · schema.prisma পড়া

পর্ব ২৯ আর ৩৪-এ বাজারঘরের পণ্য ছিল হাতে লেখা SQL table-এ। এবার সেটি Prisma schema-য় লিখি। generator বলে কী তৈরি হবে, datasource বলে কোন database, আর model বলে কোন তথ্য।

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

enum ProductStatus {
  DRAFT
  ACTIVE
  ARCHIVED
}

model Product {
  id         Int           @id @default(autoincrement())
  sku        String        @unique
  name       String
  category   String
  pricePaisa Int           @map("price_paisa")
  stock      Int           @default(0)
  status     ProductStatus @default(ACTIVE)
  createdAt  DateTime      @default(now()) @map("created_at")
  updatedAt  DateTime      @updatedAt @map("updated_at")

  @@map("products")
}
লেখামানেআসল database-এ যা হলো
@idPrimary keyproducts_pkey index
@default(autoincrement())নিজে থেকে বাড়া idnextval('products_id_seq'), একটি sequence
@uniqueমান অনন্যproducts_sku_key নামের unique index
@default(0)না দিলে এই মানcolumn default 0
@map("price_paisa")কোডে এক নাম, column-এ আরেকcolumn price_paisa
@@map("products")Model-এর table-এর নামtable products
@default(now())তৈরির সময়default CURRENT_TIMESTAMP
@updatedAtবদলের সময়column-এ কোনো default নেই
enumনির্দিষ্ট মানের তালিকানিজস্ব type "ProductStatus"

এক @ মানে একটি ঘরের ওপর নিয়ম, দুই @@ মানে পুরো model-এর ওপর। @map কেন? পর্ব ২৯-এর নিয়মটি মনে করুন: কোডে pricePaisa, database-এ price_paisa. @map ছাড়া column-এর নাম হতো pricePaisa, আর হাতে SQL লেখার সময় প্রতিবার quote দিতে হতো।

টেবিলের শেষের আগের সারিটি গুরুত্বপূর্ণ। @updatedAt-এর কাজ করে client, database নয়। সত্যিকারের run:

raw update changed updatedAt? false
client update changed updatedAt? true

অর্থাৎ $executeRaw বা অন্য কোনো টুল দিয়ে সারি বদলালে updatedAt পুরোনোই থাকে। Cache বা sync এর ওপর ভরসা করলে এটি মনে রাখা দরকার।

PrismaPostgreSQLJavaScript-এ
Stringtextstring
Intinteger (৩২ bit)number
BigIntbigintbigint
Floatdouble precisionnumber
DecimalnumericDecimal object
DateTimetimestampDate
Booleanbooleanboolean

Lab-এ ঘরটি হয়েছে timestamp without time zone. পর্ব ৩৪-এর timestamptz চাইলে DateTime @db.Timestamptz লিখতে হয়। আর table বানাতে এই পর্বে চলেছে prisma db push:

$ npx prisma db push
Loaded Prisma config from prisma7.config.ts.
Prisma schema loaded from prisma\schema.prisma.
Datasource "db": PostgreSQL database "bazarghar", schema "public" at "127.0.0.1:38939"

Your database is now in sync with your Prisma schema. Done in 153ms

db push কোনো ইতিহাস রাখে না, তাই এটি শুধু নিজের মেশিনের পরীক্ষার জন্য। Production-এর পথ হলো migration, যা পর্ব ৪০-এ আসবে। Model-এর মধ্যে সম্পর্ক আসবে পর্ব ৪১-এ।

৪ · টাকা রাখবেন কীভাবে: Decimal নাকি integer paisa

চায়ের দোকানের খাতায় কেউ “সাড়ে বারো টাকা”র দশমিক নিয়ে ভাবেন না। মনে মনে হিসাব হয় পয়সায়। কম্পিউটারের জন্যও সেটিই নিরাপদ। Float-এ টাকা রাখলে floating point-এর ভুল আসে। JavaScript-এ সত্যিকারের run:

> 0.1 + 0.2
0.30000000000000004
> 19.99 * 100
1998.9999999999998

দুটি ভালো পথ আছে: Decimal, অথবা ছোট একক অর্থাৎ minor unit-এ integer. টাকার ছোট একক পয়সা, ১ টাকা = ১০০ পয়সা। তাহলে ৳১৫০.৫০ মানে 15050.

Integer paisa (Int)Decimal
JavaScript-এসাধারণ number, যোগ-বিয়োগ নিরাপদএকটি object, + চলে না, .plus() লাগে
JSON-এসংখ্যা: 15050স্ট্রিং: "150.5"
ভালো কাজেপূর্ণ পয়সার দাম, মোট, stock-এর হিসাবহার, ভ্যাটের ভগ্নাংশ, মুদ্রা বিনিময়, গ্রামপ্রতি দর
ঝুঁকিভুল করে দশমিক ঢুকলে কেটে যেতে পারেভুলে Number() করলে নিরাপত্তা শেষ

একটি Decimal(12, 2) ঘরে '150.50' রেখে সত্যিকারের run-এ পড়া হলো:

typeof row.amount  → object
JSON.stringify     → {"id":1,"amount":"150.5"}
row.amount.plus('0.25').toString() → 150.75

খেয়াল করুন, JSON-এ "150.5" এসেছে একটি স্ট্রিং হয়ে, শেষের শূন্যটিও নেই। Frontend-কে এটি জানতে হবে। পুরো কোর্সে বাজারঘরের দাম তাই integer paisa: pricePaisa Int. দেখানোর সময়ে ১০০ দিয়ে ভাগ, হিসাবের সময়ে সব পূর্ণসংখ্যা। Decimal ভুল নয়, শুধু আমাদের দরকারের চেয়ে ভারী।

কতটুকু পর্যন্ত ধরবে: একটি হিসাব

PostgreSQL-এর integer ৩২ bit, সর্বোচ্চ 2,147,483,647. একে পয়সা ধরলে:

2,147,483,647 ÷ 100 = ৳২,১৪,৭৪,৮৩৬.৪৭

একটি পণ্যের দামের জন্য ২ কোটি টাকার বেশি যথেষ্ট। কিন্তু মাসিক বিক্রির মোট এই ঘরে ধরবে, এমন নিশ্চয়তা নেই। সেখানে BigInt বিবেচনা করুন। সীমা পেরোলে আসল run-এ error এলো P2020:

Value out of range for the type: value "2147483648" is out of range for type integer
PrismaClientKnownRequestError P2020

আসল ফাঁদ: দশমিক চুপচাপ হারিয়ে গেল

TypeScript-এর চোখে Int মানে শুধু number, তাই 150.5 compile হয়ে যায়। আমরা আশা করেছিলাম Prisma থামাবে। থামায়নি। Prisma 7.10.1 আর PostgreSQL-এ সত্যিকারের run:

create({ pricePaisa: 150.4 })                → 150
create({ pricePaisa: 150.5 })                → 150
create({ pricePaisa: 151.5 })                → 151
create({ pricePaisa: 0.30000000000000004 })  → 0

কোনো error নেই, সারি তৈরি হয়ে গেল, আর দাম গোল না হয়ে কেটে গেল। এটি এই এক সেটআপের এক run-এর ফল, অন্য version-এ আচরণ আলাদা হতে পারে। শিক্ষা একই: ভরসা রাখুন প্রবেশপথে। পর্ব ২৭-এর Zod দিয়ে z.number().int() দিলে একই ভুল status 422 হয়ে ফিরল:

$ curl -X POST /products -d '{"sku":"GHI-002", … ,"pricePaisa":620.5}'
422
{"error":{"code":"VALIDATION_FAILED", … "fields":[{"path":"pricePaisa",
 "message":"Invalid input: expected int, received number"}]}}

৫ · prisma generate আর type-সহ client

prisma generate schema পড়ে আপনার project-এ TypeScript ফাইল লেখে। আমাদের run:

$ npx prisma generate
✔ Generated Prisma Client (7.10.1) to .\generated\prisma in 21ms

generated/prisma/
  client.ts   browser.ts   enums.ts   models.ts   commonInputTypes.ts
  internal/   models/Product.ts  …      (ট্রিম করা; এই lab-এ মোট 162K)

এবার একটি সম্পূর্ণ query-র পথ দেখুন, খাতা থেকে শুরু করে হাতে আসা object পর্যন্ত। “পরে” চাপুন।

  1. Schemamodel Productনকশা
  2. Generategenerated/কোড আর type
  3. আপনার কোডfindUniquetype যাচাই
  4. SQLSELECT$1 আলাদা
  5. ObjectpricePaisaদোকানের ভাষায়
schema.prismaএকটি ঘরের দুই নাম
model Product {
  sku        String @unique
  name       String
  pricePaisa Int    @map("price_paisa")
  …
}
generated/prisma/models/Product.tsনমুনা, ছোট করে দেখানো
// আসল ফাইলে অনেক বেশি লাইন; ধারণাটি এমন
export type ProductModel = {
  id: number; sku: string; name: string;
  pricePaisa: number; stock: number; …
}
src/crud.tsএখানেই compiler পাহারা দেয়
const p = await prisma.product.findUnique({
  where:  { sku: 'DAL-001' },
  select: { id: true, name: true, pricePaisa: true },
});
query logসত্যিকারের run, ছোট করা
SELECT "id", "name", "price_paisa" FROM "products"
WHERE ("sku" = $1 AND 1=1) LIMIT $2 OFFSET $3
params ["DAL-001","1","0"]
ফলাফলসত্যিকারের run
{ "id": 1, "name": "মসুর ডাল ১ কেজি", "pricePaisa": 15000 }
  1. ধাপ ১ — নকশা। Schema-য় একটি ঘরের দুটি নাম: কোডের জন্য pricePaisa, column-এর জন্য price_paisa. এখনো কোনো কোড নেই।
  2. ধাপ ২ — Generate. prisma generate schema পড়ে TypeScript ফাইল লিখল। ডানের লেখাটি ছোট করা নমুনা। মূল কথা, Int এখানে number হলো।
  3. ধাপ ৩ — আপনার কোড। এই ধাপে কিছুই চলে না, compiler পড়ে। sku-র বানান ভুল হলে, বা select-এ অজানা ঘর দিলে, এখানেই error.
  4. ধাপ ৪ — SQL. Client SQL বানাল, যেখানে নাম এখন price_paisa. মান $1-এ আলাদা গেল, তাই parameterized query, SQL injection-এর পথ বন্ধ।
  5. ধাপ ৫ — Object. সারিটি ফিরল, আর column-এর নাম আবার pricePaisa হলো। কোডে শুধু দোকানের ভাষা, যা আপনি পর্ব ২৯-এ চেয়েছিলেন।

এর সবচেয়ে বড় উপহার হলো select অনুযায়ী ফেরত type বদলানো। নিচের block-টি generated type-এর একটি ছোট শেখার মডেল, আসল কোড নয়। চালিয়ে দেখুন, compiler কোথায় আটকায়।

type Product = {
  id: number; sku: string; name: string;
  pricePaisa: number; stock: number;
};

type Select = { [K in keyof Product]?: true };
type Payload<S> = { [K in keyof S & keyof Product]: Product[K] };

function findUnique<S extends Select>(args: {
  where: { sku: string };
  select: S;
}): Payload<S> {
  const row: Product = {
    id: 1, sku: args.where.sku, name: 'dal',
    pricePaisa: 15000, stock: 40,
  };
  return row as unknown as Payload<S>;
}

const p = findUnique({
  where: { sku: 'DAL-001' },
  select: { name: true, pricePaisa: true },
});

console.log(p.name, p.pricePaisa);
console.log(p.stock);

Compiler থামল একটি লাইনেই: prog.ts(26,15): error TS2339: Property 'stock' does not exist on type 'Payload<{ name: true; pricePaisa: true; }>'. অর্থাৎ select-এর object থেকেই ফেরত type তৈরি হয়েছে, যাতে শুধু name আর pricePaisa আছে। আগের লাইনের p.name কোনো error দেয়নি। এখানে জাদু নেই, শুধু একটি generic আর একটি mapped type. as unknown as লেখাটি এই মডেলের শর্টকাট, নিজের কাজের কোডে এটি সাবধানে ব্যবহার করবেন। ভুলটি আসল কোডে থাকলে run-এ undefined আসত, আর দাম বা stock-এর হিসাব চুপচাপ ভুল হতো। Compiler সেটি চালানোর আগেই ধরল।

আসল client-এও হুবহু একই ভাবে থামে। Prisma 7.10.1-এর generated type-এ tsc-এর সত্যিকারের run:

src/select-types.ts(8,15): error TS2339: Property 'stock' does not exist on type
  '{ name: string; pricePaisa: number; }'.
src/select-types.ts(11,32): error TS2322: Type 'string' is not assignable to type 'number'.
src/select-types.ts(14,42): error TS2561: Object literal may only specify known properties,
  but 'pricePoisha' does not exist in type 'ProductWhereInput'. Did you mean to write 'pricePaisa'?

তৃতীয় বার্তাটি দেখুন: বানান ভুল ধরে ঠিক নামটিও বলে দিল। কিন্তু এই পাহারা ততক্ষণ ঠিক, যতক্ষণ client নতুন। Prisma 7-এ db push আর migrate dev নিজে থেকে generate চালায় না। আমরা schema-য় weightGram যোগ করে শুধু db push চালালাম:

src/stale.ts(2,54): error TS2353: Object literal may only specify known properties,
  and 'weightGram' does not exist in type 'ProductSelect<DefaultArgs>'.

Table-এ ঘর আছে, client জানে না। npx prisma generate চালাতেই error গেল। Schema বদলের পরের অভ্যাস: push, তারপর generate.

৬ · CRUD আর আসল SQL দেখা

ORM-এর সবচেয়ে বড় বিপদ হলো না দেখে বিশ্বাস করা। তাই শুরুতেই query log চালু করি। log-এ emit: 'event' দিলে প্রতিটি query একটি event হয়:

const prisma = new PrismaClient({
  adapter,
  log: [{ emit: 'event', level: 'query' }, 'warn'],
});

prisma.$on('query', (e) =>
  console.log(`SQL> ${e.query}\nparams ${e.params} (${e.duration}ms)`));

এই log-এ params-সহ আসল মান থাকে, যাতে ফোন নম্বর বা ইমেইলও আসতে পারে। Production-এ params লিখবেন না (পর্ব ৩১-এর redaction).

এবার বাজারঘরের পণ্য নিয়ে সব কাজ। প্রতিটির নিচে আসল SQL, সত্যিকারের run. পড়ার সুবিধায় বারবার আসা "public"."products". অংশ ছেঁটে দেওয়া হয়েছে, আর লম্বা তালিকায় “…” বসেছে।

const dal = await prisma.product.create({
  data: { sku: 'DAL-001', name: 'মসুর ডাল ১ কেজি',
          category: 'dal', pricePaisa: 15000, stock: 40 },
});
INSERT INTO "products" ("sku","name","category","price_paisa","stock","status",
  "created_at","updated_at")
VALUES ($1,$2,$3,$4,$5,CAST($6::text AS "ProductStatus"),$7,$8) RETURNING "id", "sku", …
params ["DAL-001","মসুর ডাল ১ কেজি","dal",15000,40,"ACTIVE","2026-11-01T17:48:27.526Z", …]

=> {"id":1,"sku":"DAL-001", … ,"pricePaisa":15000,"stock":40,"status":"ACTIVE", …}

একটি query-তেই সারি ফিরল। status আর সময় দিয়েছে client নিজে, schema-র default দেখে।

const r = await prisma.product.createMany({
  data: [ /* ৬টি পণ্য, যার একটি DAL-001 ডুপ্লিকেট */ ],
  skipDuplicates: true,
});
INSERT INTO "products" (…) VALUES ($1,…,$8), ($9,…,$16), … ($41,…,$48) ON CONFLICT DO NOTHING

=> {"count":5}

৬টি সারি একটি মাত্র INSERT-এ গেল। ফলাফল শুধু সংখ্যা, সারি নয়। সারিসহ চাইলে createManyAndReturn আছে। skipDuplicates ডুপ্লিকেটটি চুপচাপ বাদ দিল, তাই ৬-এর জায়গায় ৫.

await prisma.product.findUnique({ where: { sku: 'NOPE' } });
SELECT "id", "sku", "name", … FROM "products" WHERE ("sku" = $1 AND 1=1) LIMIT $2 OFFSET $3
params ["NOPE","1","0"]

=> null

where-এ শুধু @id বা @unique ঘর চলে। না পেলে error নয়, null. select না দিলে সব ঘর আসে।

await prisma.product.update({
  where: { sku: 'TEL-001' },
  data: { stock: { increment: 20 } },
});
UPDATE "products" SET "stock" = ("stock" + $1), "updated_at" = $2
WHERE ("sku" = $3 AND 1=1) RETURNING "id", "sku", "stock", …
params [20,"2026-11-01T17:48:27.554Z","TEL-001"]

increment পড়ে নিয়ে যোগ করে না। হিসাবটি SQL-এর ভেতরেই হয়, তাই দুই অনুরোধ একসাথে এলেও যোগ হারায় না। updated_at client নিজে বসিয়েছে, যা অংশ ৩-এ দেখেছেন।

await prisma.product.delete({ where: { sku: 'SUG-001' } });
DELETE FROM "products" WHERE ("sku" = $1 AND 1=1) RETURNING "id", "sku"

=> {"id":9,"sku":"SUG-001"}

মুছে ফেলা সারিটিই ফিরে আসে। অনেক সারির জন্য আছে updateMany আর deleteMany.

নিচে একই ধরনের query আপনি নিজে চালাতে পারেন। এটি পাতার ভেতরের SQLite, তাই সারিগুলো আসল PostgreSQL-এর lab-এর মতো সাজানো হলেও dialect একটু আলাদা। মান বদলে দেখুন।

-- findMany({ where: { category: { in: ['rice','dal'] },
--   pricePaisa: { lte: 45000 }, stock: { gt: 0 } },
--   select: {...}, orderBy: [{pricePaisa:'desc'},{id:'asc'}],
--   take: 2, skip: 1 })
SELECT id, name, price_paisa FROM products
WHERE category IN ('rice', 'dal')
  AND price_paisa <= 45000 AND stock > 0
ORDER BY price_paisa DESC, id ASC
LIMIT 2 OFFSET 1;

-- update({ where: {sku}, data: { stock: { increment: 20 } } })
UPDATE products SET stock = stock + 20 WHERE sku = 'TEL-001'
RETURNING sku, stock;

-- upsert: DAL-001 already exists, so the UPDATE part runs
INSERT INTO products (sku, name, category, price_paisa, stock)
VALUES ('DAL-001', 'Masoor dal 1kg', 'dal', 15000, 5)
ON CONFLICT (sku) DO UPDATE SET stock = stock + 5
RETURNING sku, stock;

-- upsert: SUG-001 is new, so the INSERT part runs
INSERT INTO products (sku, name, category, price_paisa, stock)
VALUES ('SUG-001', 'Sugar 1kg', 'sugar', 14000, 5)
ON CONFLICT (sku) DO UPDATE SET stock = stock + 5
RETURNING sku, stock;

পাতার run-এ প্রথম query দিল 5 Mung dal 1kg 19000 আর 1 Masoor dal 1kg 15000, ২টি সারি। শর্ত মেলে ৩টি পণ্যে (42000, 19000, 15000). OFFSET 1 সবচেয়ে দামিটি বাদ দিল, তাই আসল PostgreSQL-এর ফলের সাথে হুবহু মিলেছে। UPDATE দিল TEL-001 20, কারণ 0 + 20. প্রথম upsert দিল DAL-001 45, অর্থাৎ DO UPDATE চলেছে (40 + 5). দ্বিতীয়টি দিল SUG-001 5, অর্থাৎ INSERT. একই SQL, ফল নির্ভর করেছে সারিটি আগে ছিল কি না তার ওপর।

upsert: থাকলে বদলানো, না থাকলে নতুন বানানো

Upsert সবচেয়ে ভুল বোঝা কাজগুলোর একটি। সরবরাহকারীর তালিকা থেকে পণ্য আসছে: থাকলে stock বাড়াতে হবে, না থাকলে নতুন পণ্য বসাতে হবে। শর্ত মিললে Prisma কাজটি একটি মাত্র SQL-এ করে, ON CONFLICT দিয়ে। আগে DAL-001-এ stock ছিল 40, আর sequence-এর পরের id ছিল 8.

1INSERT INTO products (sku, …, stock)2VALUES ($1, …, 5)3ON CONFLICT (sku)4DO UPDATE SET stock = stock + 55RETURNING id, sku, stock;
অবস্থা
চাওয়া sku
sequence যে id দিল
DAL-001 stock
SUG-001 stock
  1. Client একবার ভাবল। where-এ একটি unique ঘর, আর create-এ তার মান একই (DAL-001). তাই সে SQL-এ একটি SELECT না করে সোজা ON CONFLICT পাঠাবে।
  2. INSERT শুরু। PostgreSQL সারি বানাতে গিয়ে আগে sequence থেকে id নিল: 8. সংঘাত তখনো ধরা পড়েনি।
  3. সংঘাত। Unique index products_sku_key-এ DAL-001 আগেই আছে। নতুন সারি বসবে না, কাজটি ON CONFLICT-এ গেল।
  4. DO UPDATE. পুরোনো সারিতে stock = 40 + 5 = 45. কিন্তু id 8 আর ফেরত যায় না, sequence পেছনে হাঁটে না। আসল run-এর ফল ছিল {"sku":"DAL-001","stock":45}.
  5. দ্বিতীয় ডাক, নতুন পণ্য। এবার SUG-001. Sequence দিল 9, কোনো সংঘাত নেই, তাই update অংশটি চলেইনি। stock হলো create-এর 5.
  6. ফলাফল। আসল run-এ id ছিল 9, যদিও টেবিলে পণ্য ৭টি। আগের createMany-র বাদ পড়া ডুপ্লিকেট নিল id 7, এই upsert নিল 8. তাই id-র ফাঁক কোনো ভুল নয়, id গুনে পণ্যের সংখ্যা বলবেন না।

শর্ত না মিললে কী হয়? Docs-এর শর্ত এগুলো: create আর update-এ nested কাজ নেই, where-এ একটিই unique ঘর, আর সেই ঘরের মান where ও create-এ এক। আমরা ইচ্ছা করে create-এ ভিন্ন sku দিলাম। আসল log:

SELECT "id" FROM "products" WHERE ("sku" = $1 AND 1=1) OFFSET $2
INSERT INTO "products" (…) VALUES (…) RETURNING "id"
SELECT "id", "sku", "stock" FROM "products" WHERE "id" = $1 LIMIT $2 OFFSET $3
COMMIT

একের জায়গায় তিনটি query, আর মাঝখানে সময়ের ফাঁক। এই ফাঁকের ফল কী, একটি run-এ মাপা হলো। একই পণ্যে ১০টি অনুরোধ একসাথে পাঠানো হলো, দুই পথে:

native upsert ×10            → ok 10, stock = 10
findUnique, তারপর create ×10 → ok 1, rejected 9, codes [ 'P2002' ], stock = 1

একই কাজ, অথচ নিজে হাতে লেখা “দেখে তারপর বানাও” ১০টির ৯টিতে ব্যর্থ, আর ৯টি যোগ হারিয়ে গেছে। এই race-এর পূর্ণ আলোচনা পর্ব ৪৩-এ। এখনকার নিয়ম: unique constraint-ই আসল পাহারাদার, আগের SELECT নয়।

৭ · পড়া: where, select, orderBy, take, skip আর cursor

পর্ব ৩৪-এ filter আর pagination-এর নিয়ম শিখেছেন। এখানে দেখুন Prisma-র ভাষায় সেগুলো কী হয়। সব SQL এই lab-এর আসল log থেকে ছোট করা।

PrismaSQL
{ category: 'dal' }"category" = $1
{ category: { in: [...] } }"category" IN ($1,$2)
{ pricePaisa: { lte: 45000 } }"price_paisa" <= $3
{ stock: { gt: 0 } }"stock" > $4
{ name: { contains: 'চা পাতা' } }"name"::text LIKE ('%' || $2 || '%')
{ sku: { startsWith: 'tel', mode: 'insensitive' } }"sku" ILIKE ($1 || '%')
{ OR: [a, b] }(a OR b)

এক where-এর কয়েকটি শর্ত মানে AND. contains সুন্দর দেখায়, কিন্তু সামনে % থাকায় সাধারণ B-tree index কাজে আসে না, যা পর্ব ৩৪-এ দেখেছেন। বড় টেবিলে এর জন্য full-text search ভাবতে হয়।

একসাথে সব ব্যবহার করলে এমন, সত্যিকারের run:

const rows = await prisma.product.findMany({
  where: {
    category: { in: ['rice', 'dal'] },
    pricePaisa: { lte: 45000 },
    stock: { gt: 0 },
  },
  select: { id: true, name: true, pricePaisa: true },
  orderBy: [{ pricePaisa: 'desc' }, { id: 'asc' }],
  take: 2,
  skip: 1,
});
SELECT "id", "name", "price_paisa" FROM "products"
WHERE ("category" IN ($1,$2) AND "price_paisa" <= $3 AND "stock" > $4)
ORDER BY "price_paisa" DESC, "id" ASC LIMIT $5 OFFSET $6
params ["rice","dal",45000,0,"2","1"]

=> [{"id":5,"name":"মুগ ডাল ১ কেজি","pricePaisa":19000},
    {"id":1,"name":"মসুর ডাল ১ কেজি","pricePaisa":15000}]

take হলো LIMIT, skip হলো OFFSET. select শুধু দরকারি ঘর আনে, আর ফোনের network-এ এই ছোট্ট লাইনটিই payload কমায়। উল্টো কাজের জন্য আছে omit, তবে দুটো একসাথে চলে না।

ভেতরের নিয়মগুলো চোখে দেখতে নিচে একটি ছোট নকল prisma.product. এটি শেখার মডেল, আসল Prisma নয়, তবে এর শেষ লাইনটি আসল আচরণটিই দেখায়।

const rows = [
  { id: 1, sku: 'DAL-001', category: 'dal',  pricePaisa: 15000, stock: 40 },
  { id: 2, sku: 'CHL-001', category: 'rice', pricePaisa: 42000, stock: 25 },
  { id: 3, sku: 'CHL-002', category: 'rice', pricePaisa: 47500, stock: 8 },
  { id: 4, sku: 'TEL-001', category: 'oil',  pricePaisa: 18500, stock: 0 },
  { id: 5, sku: 'DAL-002', category: 'dal',  pricePaisa: 19000, stock: 12 },
];
const ok = (value, f) => {
  if (f === undefined) return true;          // undefined মানে কোনো শর্তই নেই
  if (typeof f !== 'object') return value === f;
  if (f.in && !f.in.includes(value)) return false;
  if (f.gt !== undefined && !(value > f.gt)) return false;
  return true;
};
const product = {
  findMany({ where = {}, select, orderBy, take, skip = 0 }) {
    let out = rows.filter((r) =>
      Object.entries(where).every(([k, f]) => ok(r[k], f)));
    if (orderBy) {
      const [[k, dir]] = Object.entries(orderBy);
      out = [...out].sort((a, b) => (dir === 'asc' ? a[k] - b[k] : b[k] - a[k]));
    }
    out = out.slice(skip, take === undefined ? undefined : skip + take);
    if (!select) return out;
    return out.map((r) => Object.fromEntries(Object.keys(select).map((k) => [k, r[k]])));
  },
};
console.log(product.findMany({
  where: { category: { in: ['rice', 'dal'] }, stock: { gt: 0 } },
  select: { sku: true, pricePaisa: true },
  orderBy: { pricePaisa: 'desc' }, take: 2, skip: 1,
}));
const sku = undefined;                       // ভুল করে খালি
console.log(product.findMany({ where: { sku } }).length, 'rows');

প্রথম ফল [ { sku: 'CHL-001', pricePaisa: 42000 }, { sku: 'DAL-002', pricePaisa: 19000 } ]. শর্তে মিলল ৪টি সারি: 47500, 42000, 19000, 15000. skip: 1 প্রথমটি বাদ দিল, take: 2 পরের দুটি নিল, আর select শুধু দুটি ঘর রাখল। দ্বিতীয় লাইন 5 rows, যদিও আমরা একটি মাত্র sku খুঁজছিলাম। sku ছিল undefined, তাই শর্তটিই বাদ পড়ল, আর ৫টি সারিই ফিরল। এই মডেলের ক্রম আসল database-এ হয় SQL-এর ভেতরে, JavaScript-এ নয়।

undefined মানে “এই শর্ত নেই”, “মেলে না” নয়। আসল Prisma-য় সত্যিকারের run: ৭টি পণ্যের টেবিলে findMany({ where: { sku: undefined } }) দিল ৭টি সারি, আর findFirst দিল প্রথম পণ্যটি। Prisma-র docs-এর উদাহরণে deleteMany({ where: { id: undefined } }) সব সারি মুছে দেয়। ভাবুন where: { ownerId: req.user?.id }, আর login ছাড়া request এলো: সবার data বেরিয়ে যাবে। তাই নিশ্চিত হওয়া মান ছাড়া where-এ কিছু দেবেন না। Docs-এ Prisma.skip আর preview option strictUndefinedChecks-এর কথাও আছে।

Cursor: পর্ব ৩৪-এর সাথে মিলিয়ে

Prisma-য় cursor লেখা সহজ: cursor-এ শেষ সারির id, আর skip: 1 দিয়ে সেই সারিটি বাদ। না হলে সেটি আবার আসে।

const page2 = await prisma.product.findMany({
  take: 2, skip: 1,
  cursor: { id: 2 },
  orderBy: { id: 'asc' },
  select: { id: true, sku: true },
});
SELECT "id", "sku" FROM "products"
WHERE "id" >= (SELECT "id" FROM "products" WHERE ("id") = ($1))
ORDER BY "id" ASC LIMIT $2 OFFSET $3
params [2,"2","1"]

=> [{"id":3,"sku":"CHL-002"},{"id":4,"sku":"TEL-001"}]

দেখুন, আসল SQL পর্ব ৩৪-এর সরল WHERE id > ? নয়। এতে একটি subquery আর OFFSET 1 আছে। ছোট অফসেট, তাই গভীরতায় খরচ বাড়ে না, তবে cursor-এ দেওয়া ঘরটি অনন্য হতে হয়। পর্ব ৩৪-এর (created_at, id) ক্রমের জন্য তাই একটি compound unique লাগবে, নয়তো নিজের where লিখতে হবে। আর hasMore এর জন্য পর্ব ৩৪-এর limit + 1 কৌশলই চলে। Client-এর পাঠানো object কখনো সরাসরি orderBy-এ দেবেন না, পর্ব ৩৪-এর allowlist এখানেও দরকার।

৮ · P2002, P2025 আর API error

পর্ব ২৮-এর error handling-এ toAppError লিখেছিলেন, যা ভেতরের ভুলকে একটি ভাষায় আনে। সেখানে কথা দেওয়া ছিল, Prisma-য় unique violation-এর code হবে P2002. Prisma-র এই ভুলগুলো আসে PrismaClientKnownRequestError হয়ে, যার code স্থির।

CodeকখনHTTP
P2002Unique ঘরে ডুপ্লিকেট409 Conflict
P2025update, delete, findUniqueOrThrow সারি পায়নি404 Not Found
P2020মান ঘরের সীমার বাইরেআগেই Zod-এ ধরুন; তবু এলে 422
অচেনাবাকি সব500, সাধারণ বার্তা

এবার সবচেয়ে সূক্ষ্ম জায়গা। প্রায় সব পুরোনো টিউটোরিয়াল আর AI লেখে e.meta.target, যাতে ঘরের নাম থাকত। Docs-এর সাধারণ error পাতাতেও উদাহরণটি এখনো আছে। Driver adapter-এ আমাদের সত্যিকারের run-এ দেখা গেল অন্য আকার (trim করা):

name: PrismaClientKnownRequestError | code: P2002 | clientVersion: 7.10.1
instanceof: true
meta: {
  driverAdapterError: DriverAdapterError: UniqueConstraintViolation {
    cause: {
      originalCode: '23505',
      originalMessage: 'duplicate key value violates unique constraint "products_sku_key"',
      kind: 'UniqueConstraintViolation',
      constraint: { index: 'products_sku_key' },
      table: 'products'
    }
  },
  modelName: 'Product'
}

meta.target নেই। পর্ব ২৮-এ বলা SQLSTATE 23505 ভেতরে আছে, আর index-এর নামও আছে। তাই meta-র আকারের ওপর ব্যবসার নিয়ম দাঁড় করাবেন না। নিচে দুই ধরনের লেখার তুলনা। এটিও শেখার মডেল: class-টি নকল, তবে meta-র আকার ওপরের আসল run থেকে নেওয়া।

class KnownError extends Error {               // নকল, আসলটি generated client-এ
  constructor(code, meta) { super('prisma detail'); this.code = code; this.meta = meta; }
}
class AppError extends Error {
  constructor(status, code, message) {
    super(message); this.status = status; this.code = code;
  }
}
function toAppError(err) {
  if (err instanceof AppError) return err;
  if (err instanceof KnownError) {
    if (err.code === 'P2002') return new AppError(409, 'DUPLICATE_SKU', 'SKU exists');
    if (err.code === 'P2025') return new AppError(404, 'PRODUCT_NOT_FOUND', 'Not found');
  }
  return null;                                 // অচেনা মানে bug
}
const reply = (err) => {
  const k = toAppError(err);
  return { status: k?.status ?? 500, code: k?.code ?? 'INTERNAL',
    message: k?.message ?? 'Something went wrong on our side' };
};
const dup = new KnownError('P2002', {
  modelName: 'Product',
  driverAdapterError: { cause: { originalCode: '23505',
    constraint: { index: 'products_sku_key' }, table: 'products' } },
});
console.log('old habit :', dup.meta.target);
console.log('real path :', dup.meta.driverAdapterError.cause.constraint.index);
console.log(reply(dup));
console.log(reply(new KnownError('P2025', { modelName: 'Product' })));
console.log(reply(new KnownError('P2010', {})));
console.log(reply(new TypeError('secret table name in here')));

old habit : undefined বলছে পুরোনো পথে কিছুই নেই, আর real path : products_sku_key বলছে তথ্যটি অন্য জায়গায়। তারপর P2002 হলো { status: 409, code: 'DUPLICATE_SKU', … }, আর P2025 হলো { status: 404, code: 'PRODUCT_NOT_FOUND', … }. শেষ দুটি লক্ষ করুন: অচেনা P2010 আর সাধারণ TypeError, দুটোই { status: 500, code: 'INTERNAL', message: 'Something went wrong on our side' }. TypeError-এর ভেতরের “secret table name” বাইরে যায়নি। আর সিদ্ধান্ত হয়েছে শুধু code দেখে, meta দেখে নয়, তাই ভেতরের আকার বদলালেও handler ভাঙে না।

একটি ডুপ্লিকেট SKU কীভাবে database থেকে উঠে 409 হয়, সেটি স্তর ধরে দেখুন।

  1. RequestPOSTZod পাস
  2. RepositorycreateINSERT
  3. PostgreSQL23505থামাল
  4. AdapterP2002অনুবাদ
  5. toAppError409সিদ্ধান্ত
  6. ResponseJSONনিরাপদ বার্তা
HTTPএকই SKU দ্বিতীয়বার
POST /products
{ "sku": "GHI-001", "name": "again",
  "category": "ghee", "pricePaisa": 62000 }
repositoryসে কিছুই ধরে না
create: (p) => db.product.create({ data: p, select: fields })
PostgreSQLসত্যিকারের বার্তা
duplicate key value violates unique constraint
"products_sku_key"
Prismaসত্যিকারের ফল
PrismaClientKnownRequestError
code: P2002
message: (ফাইলের path আর কোডের লাইনসহ লম্বা লেখা)
toAppErrorএক জায়গার সিদ্ধান্ত
if (err.code === 'P2002')
  return new AppError(409, 'DUPLICATE_SKU', 'এই SKU আগেই আছে', …)
HTTPসত্যিকারের run
409
{"error":{"code":"DUPLICATE_SKU","message":"এই SKU আগেই আছে",
 "fields":[{"path":"sku","message":"এই SKU আগেই আছে"}]}}
  1. ধাপ ১ — Request. Zod-এর চোখে সব ঠিক: GHI-001 আকারে বৈধ। ডুপ্লিকেট কি না Zod জানে না, কারণ সে database দেখে না।
  2. ধাপ ২ — Repository. create চলল। এখানে আগে findUnique করে দেখা হয় না, কারণ সেই যাচাই race-এ হেরে যায়।
  3. ধাপ ৩ — PostgreSQL. Unique index সারিটি থামাল, SQLSTATE 23505. এটিই একমাত্র জায়গা, যা একসাথে আসা দুই অনুরোধেও ঠকে না।
  4. ধাপ ৪ — Adapter আর client. pg-এর ভুলটি P2002 হলো। এর message-এ ফাইলের path আর কোডের লাইন থাকে, যা আমাদের run-এই দেখা গেছে। এটি কখনো বাইরে পাঠানো যাবে না।
  5. ধাপ ৫ — toAppError. কেন্দ্রীয় handler code দেখে সিদ্ধান্ত নিল: 409. Repository বা controller-এ আলাদা try/catch লাগল না।
  6. ধাপ ৬ — Response. Client পেল স্থির code আর ভদ্র বার্তা। Log-এ রইল আসল কারণ: {"status":409,"code":"DUPLICATE_SKU","prisma":"P2002"}.

আসল Express অ্যাপে পাঁচটি request-এর সত্যিকারের run (Express 5.3.0, ট্রিম করা). id 62 এসেছে, কারণ আগের পরীক্ষাগুলো sequence খরচ করেছে:

$ POST   /products            {"sku":"GHI-001", …}     → 201 {"id":62,"sku":"GHI-001", … }
$ POST   /products            {"sku":"GHI-001", …}     → 409 {"error":{"code":"DUPLICATE_SKU", … }}
$ PATCH  /products/GHI-001/price {"pricePaisa":64500}  → 200 {"id":62, … ,"pricePaisa":64500, … }
$ PATCH  /products/NOPE-999/price {"pricePaisa":100}   → 404 {"error":{"code":"PRODUCT_NOT_FOUND", … }}
$ DELETE /products/NOPE-999                            → 404 {"error":{"code":"PRODUCT_NOT_FOUND", … }}

Client বানাতে log-এ 'error' দিলে Prisma প্রতিটি P2002 আর P2025-ও prisma:error লাইনে ছাপে, পুরো path-সহ। অথচ এগুলো স্বাভাবিক ঘটনা, 500 নয়। আমরা তাই 'error' বাদ দিয়েছি, আর লেখার দায়িত্ব central handler-কে দিয়েছি।

৯ · একটিই client, hot reload আর repository

Client-এর ভেতরে থাকে একটি connection pool, অর্থাৎ আগে থেকে খোলা কিছু সংযোগ। তিনটি CNG-র একটি স্ট্যান্ড ভাবুন: যাত্রী অপেক্ষা করেন, গাড়ি ফিরলে পরের জন ওঠেন। Prisma 7-এ pool-টি pg-এর, তাই তার ডিফল্টই চলে: max ১০টি সংযোগ। Prisma 6-এর ডিফল্ট আলাদা ছিল, আর docs সতর্ক করে যে pg-এর সংযোগের ডিফল্ট timeout নেই।

একটি হিসাব: pool কতটা কাজ নিতে পারে

৩০টি query, প্রতিটি ১০০ ms. Pool-এ cটি সংযোগ থাকলে আদর্শ সময় হবে ⌈30 ÷ c⌉ × 100 ms:

maxহিসাবআদর্শএক run-এ মাপাসংযোগ
3⌈30 ÷ 3⌉ × 100১,০০০ ms১,১৪২ ms3
10⌈30 ÷ 10⌉ × 100৩০০ ms৩৭৭ ms10

বাড়তি সময়টি নতুন সংযোগ খোলার আর Prisma-র নিজের খরচ, এবং এটি একটি মেশিনের এক run-এর মাপ। Pool ঠিক করতে হলে adapter-এ দিন:

const adapter = new PrismaPg({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 5000,
});

Hot reload-এ সংযোগ জমে কেন

Client মানে pool, আর pool মানে খোলা সংযোগ। কিছু dev server ফাইল বদলালে পুরো process চালু রেখে module আবার চালায়, যেমন Next.js-এর dev server (পর্ব ৫১). তখন new PrismaClient() আবার চলে, কিন্তু আগের client বন্ধ হয় না। tsx watch বা node --watch সাধারণত পুরো process নতুন করে চালায়, তাই সেখানে এই সমস্যা কম। আমরা একই process-এ module বারবার import করে এই ঘটনাটি অনুকরণ করলাম, প্রতিবার ১০টি query একসাথে। সত্যিকারের run, PostgreSQL-এর max_connections 100:

সাধারণ module                            singleton
reload  1: clients=1 connections=10      reload  1: clients=1 connections=10
reload  2: clients=2 connections=20      reload  2: clients=1 connections=10
…                                        …
reload  9: clients=9 connections=90      reload  9: clients=1 connections=10
reload 10: FAILED P2010 Raw query failed.      reload 10: clients=1 connections=10
  Code: `53300`. sorry, too many clients already      …

বাঁয়ে দশম reload-এই database নতুন সংযোগ নিতে অস্বীকার করল। হিসাবটি সহজ: ৯ × ১০ = ৯০, পরীক্ষার নিজের ১টি, আর দশম client-এর ১০টির কিছু। ডানে সংখ্যাটি নড়েনি। সমাধান হলো singleton: client-কে এমন জায়গায় রাখা, যা module আবার চললেও টিকে থাকে, অর্থাৎ globalThis:

import 'dotenv/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../../generated/prisma/client.ts';

function createClient() {
  const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
  return new PrismaClient({
    adapter,
    log: [{ emit: 'event', level: 'query' }, 'warn'],
  });
}

type Client = ReturnType<typeof createClient>;
const globalForPrisma = globalThis as unknown as { prisma?: Client };

export const prisma = globalForPrisma.prisma ?? createClient();

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

Adapter ছাড়া client বানাতে গেলে আসল run-এ বার্তা স্পষ্ট:

PrismaClientInitializationError: PrismaClient was instantiated without any options.
A driver adapter is required to connect to your database.

আর বন্ধ করার সময় সংযোগ ফেরত দিন। পর্ব ২৮-এর graceful shutdown-এ SIGTERM পেলে await prisma.$disconnect() ডাকুন। আমাদের run-এ সব client-এর $disconnect-এর পরে সংযোগ ছিল ০।

পর্ব ২৯-এর repository-তে জোড়া

পর্ব ২৯-এ repository-র চুক্তি ছিল interface, আর আসল কোড ছিল SQLite-এ। এবার একই ধরনের চুক্তির নিচে Prisma বসে। Service কিছুই টের পায় না।

import { Prisma, PrismaClient } from '../generated/prisma/client.ts';
import type { ProductRepository } from './product.repository.ts';

const fields = {
  id: true, sku: true, name: true, category: true,
  pricePaisa: true, stock: true,
} satisfies Prisma.ProductSelect;

export function prismaProductRepository(db: PrismaClient): ProductRepository {
  return {
    findBySku: (sku) =>
      db.product.findUnique({ where: { sku }, select: fields }),

    list: ({ category, limit, after }) =>
      db.product.findMany({
        where: { status: 'ACTIVE', category },
        select: fields,
        orderBy: { id: 'asc' },
        take: limit,
        ...(after ? { cursor: { id: after }, skip: 1 } : {}),
      }),

    create: (p) => db.product.create({ data: p, select: fields }),

    async setPrice(sku, pricePaisa) {
      try {
        return await db.product.update({
          where: { sku }, data: { pricePaisa }, select: fields,
        });
      } catch (e) {
        if (e instanceof Prisma.PrismaClientKnownRequestError
            && e.code === 'P2025') return null;
        throw e;
      }
    },
  };
}

চারটি সিদ্ধান্ত খেয়াল করুন। select: fields রাখায় createdAt-এর মতো ভেতরের ঘর বাইরে যায় না। db parameter-এ আসে, তাই dependency injection অটুট। P2025 এখানে null হয়েছে, কারণ পর্ব ২৯-এর চুক্তি বলে “না পেলে null”, আর 404-এর সিদ্ধান্ত repository-র নয়। আর list-এ category undefined হলে filter চলে যায়, এখানে সেটিই আমরা চাই। ঐচ্ছিক filter-এ এটি সুবিধা, বাধ্যতামূলক শর্তে এটি বিপদ।

সত্যিকারের run-এ category=dal&limit=1 দিল "hasMore":true,"next":1, আর after=1 দিয়ে পরের পাতা দিল "hasMore":false.

Prisma Studio: npx prisma studio browser-এ table দেখা আর সম্পাদনার একটি টুল খোলে। শেখার সময়ে কাজে লাগে। মনে রাখবেন, এতে হাত দিয়ে data বদলানো যায়, তাই production-এর URL দিয়ে খুলবেন না। এই পর্বের lab-এ এটি খোলা হয়নি, শুধু --help দেখা হয়েছে।

আরও গভীরে: Prisma “data mapper”-এর কাছাকাছি হলেও row-গুলো সাধারণ object, method নেই। generate আসলে compile-time metaprogramming: schema একটি ছোট ভাষা, আর generator তার compiler. Type-গুলো mapped আর conditional type-এ তৈরি, যা পর্ব ৩৭-এ দেখেছেন। তাই বড় schema-য় editor কিছুটা ধীর হতে পারে। PostgreSQL-এ sequence transaction-এর বাইরে চলে, ফলে nextval rollback হয় না। এজন্যই upsert-এর stepper-এ id নষ্ট হয়েছে।

পরীক্ষা ও ইন্টারভিউতে:

  • findUnique আর findFirst-এর পার্থক্য? প্রথমটি শুধু unique ঘরে, দ্বিতীয়টি যেকোনো where-এ প্রথম মিল।
  • update সারি না পেলে কী হয়? P2025 ছোড়ে, null দেয় না।
  • Prisma 7-এ driver adapter কেন? Rust engine সরে যাওয়ায় আসল সংযোগ করে Node-এর driver, adapter তার জোড়া।
  • টাকা কেন Float-এ নয়? দ্বিমিক দশমিক অনেক মান ঠিক ধরে না। Integer paisa বা Decimal নিন।
  • schema.prisma বদলালে কী চালাবেন? Table-এর জন্য migrate বা push, client-এর জন্য prisma generate.

১০ · AI-এর যুগে Prisma

Prisma দ্রুত বদলেছে, আর AI-এর শেখা উপাদানে পুরোনো version-ই বেশি। তাই এখানে AI খুব আত্মবিশ্বাসে পুরোনো কোড লেখে। নিচের প্রতিটি আমরা 7.10.1-এ চালিয়ে বা docs-এ মিলিয়ে দেখেছি।

AI যা লেখেযা হয়ঠিক পথ
new PrismaClient()PrismaClientInitializationErroradapter-সহ
schema-য় url = env(...)validate errorconfig ফাইলে
prisma.$use(...)আমাদের run-এ $use undefined, API সরানো হয়েছে$extends
e.meta.targetadapter-এ undefinede.code ধরে সিদ্ধান্ত
price Floatদশমিকের ভুলpricePaisa Int
আগে খুঁজে, তারপর create১০-এ ৯ P2002, একটি run-এ@unique, upsert
catch (e) { return [] }ভুল লুকিয়ে গেলcentral handler-এ পাঠান

AI-কে কাজ দেওয়ার সময় version আর সীমা আগেই বলে দিন, যাতে সে অনুমানে পুরোনো কোড না লেখে:

আমি Prisma 7.10.1 (stable), PostgreSQL আর @prisma/adapter-pg ব্যবহার করি। Project ESM. generator হলো prisma-client, আর client import হয় ../generated/prisma/client.ts থেকে। Config prisma7.config.ts-এ। দাম integer paisa-তে (pricePaisa Int). নিচের কাজটি করো: [এখানে লিখুন] নিয়ম: ১. কোনো package প্রস্তাব করলে আগে কেন লাগবে বলো। ২. এই version-এ নেই এমন API ব্যবহার করবে না। সন্দেহ হলে prisma.io-র কোন পাতা দেখব, সেটি বলো। ৩. P2002 আর P2025 কোথায় ধরা হবে দেখাও। ভুল লুকিয়ে ফাঁকা ফল দেবে না। ৪. where-এ যে মান undefined হতে পারে, তা আলাদা করে দেখাও। ৫. কোডের সাথে এটি যে SQL বানাবে বলে মনে করো, সেটি লেখো, আমি query log-এ মিলিয়ে নেব।

আরেকটি কথা। Prisma-র CLI 6.15 থেকে AI agent চিনতে পারে, আর data মুছে দিতে পারে এমন কমান্ড সে নিজে থেকে চালায় না। আমাদের lab-এই db push --accept-data-loss থেমে গিয়ে মানুষের স্পষ্ট সম্মতি চেয়েছে। এটি ভালো নকশা, তবে একমাত্র পাহারা নয়। Production-এর URL কখনো AI-এর হাতে দেবেন না, আর সম্মতির ঘরে AI-কে নিজে লিখতে দেবেন না।

AI-এর লেখা Prisma কোড মেলানোর চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১Prisma কেন নিজের Rust engine থেকে সরে এলো, আর driver adapter বাধ্যতামূলক করল? এতে কী পাওয়া গেল, কী দিতে হলো?

Prisma-র শুরুর বছরগুলোতে client-এর সাথে একটি আলাদা Rust binary চলত। JavaScript থেকে query যেত সেই binary-তে, সে SQL বানাত, database-এ যেত, আর ফল JSON হয়ে ফিরত। নকশাটির একটি বড় সুবিধা ছিল: এক engine অনেক ভাষার client-এর কাজে লাগানো যেত, আর Rust দ্রুত।

কিন্তু বাস্তবে কয়েকটি খরচ জমল। প্রথমত, প্রতিটি OS আর CPU-র জন্য আলাদা binary লাগত, যা deployment-এ “engine not found” ধরনের সমস্যা আনত। দ্বিতীয়ত, binary-র আকার serverless আর edge পরিবেশে সমস্যা করত, যেখানে bundle ছোট রাখতে হয় আর cold start গোনা হয়। তৃতীয়ত, JavaScript আর Rust-এর মাঝে প্রতিবার data পার করা বিনা খরচে হয় না। Prisma-র নিজের changelog জানায়, 7.0.0 (নভেম্বর ২০২৫) থেকে Rust-free client ডিফল্ট হয়েছে।

দাম কী? সংযোগের দায়িত্ব এখন Node-এর driver-এর। তাই pool-এর ডিফল্ট pg-এর, Prisma 6-এর নয়। Docs সতর্ক করে যে pg-এর সংযোগের ডিফল্ট timeout নেই, আর SSL সার্টিফিকেট যাচাইয়ের আচরণও বদলেছে। ফলে একটি পুরোনো অ্যাপ version বদলালে ঠিকমতো চললেও, চাপের সময়ে অন্যরকম আচরণ করতে পারে। আর আগেই দেখেছেন, e.meta-র আকারও বদলে গেছে।

এখানে একটি বড় প্রকৌশল-শিক্ষা আছে। একটি স্তর সরালে তার করা কাজগুলো উধাও হয় না, সেগুলো অন্য কারও ঘাড়ে যায়। Migration-এর কাজ এখনো একটি আলাদা schema engine-এ, তাই “Rust-free” কথাটিও পুরো CLI-র জন্য নয়।

মূল উপলব্ধি: Upgrade notes শুধু “নতুন কী” নয়, “কার দায়িত্ব কোথায় সরল” তার তালিকা। বড় version বদলে pool, timeout আর error-এর আকার নিজে মাপুন।

প্র ০২টাকার জন্য integer paisa আর Decimal-এর মধ্যে সিদ্ধান্ত কীভাবে নেবেন? একটি ছোট কাটাকাটি কি সত্যিই এত বড় ব্যাপার?

IEEE 754 মান (১৯৮৫) দশমিক সংখ্যাকে দ্বিমিকে রাখে। ১/৩ যেমন দশমিকে শেষ হয় না, 0.1 তেমনি দ্বিমিকে শেষ হয় না। তাই 0.1 + 0.2-তে ছোট ভুল আসে। একটি ভুলে ক্ষতি নেই, কিন্তু লাখ লেনদেনে জমতে থাকে।

সবচেয়ে বেশি উদ্ধৃত ঘটনাটি Vancouver Stock Exchange-এর। ১৯৮২ সালে সূচক শুরু হয়েছিল ১,০০০-এ। প্রতিটি লেনদেনের পরে সূচকের মান তিন দশমিকে গোল না করে কেটে ফেলা হতো। প্রতিবার ক্ষতি সামান্য, কিন্তু দিনে হাজার হাজার বার। প্রচলিত বিবরণ অনুযায়ী প্রায় দুই বছরে সূচক নেমে আসে ৫০০-র ঘরে, অথচ ঠিক হিসাবে তা থাকার কথা ছিল হাজারের ওপরে। সেই কাটাকাটিই আমাদের 150.5 থেকে 150 হওয়ার ঘটনার বড় রূপ।

সিদ্ধান্তের প্রশ্নটি হলো: আপনার ক্ষুদ্রতম একক কী? ISO 4217 টাকার জন্য দুই দশমিক ধরে, তাই পয়সার নিচে হিসাব না থাকলে integer সবচেয়ে সরল। যোগ-বিয়োগ নিখুঁত, JSON সংখ্যা থাকে, কোনো বাড়তি library লাগে না। কিন্তু ধরুন ৭.৫% ভ্যাট, গ্রামপ্রতি দর, বা মুদ্রা বিনিময়ের হার পয়সার ভেতরে থাকে। সেখানে Decimal, অথবা স্পষ্ট গোল করার নিয়ম লাগে। গোল করা নিজেই একটি ব্যবসার সিদ্ধান্ত: প্রতি পণ্যে, না মোটে? উত্তর একবার ঠিক করে লিখে রাখতে হয়।

আরেকটি বিষয় হলো সীমা। ৩২ bit-এ প্রায় ২ কোটি ১৪ লাখ টাকা। পণ্যের দামে যথেষ্ট, মোট বিক্রিতে নয়। JavaScript-এর number নিজে নিরাপদ পূর্ণসংখ্যা ধরে 253 পর্যন্ত, তার ওপরে BigInt.

মূল উপলব্ধি: টাকা মানে শুধু সংখ্যা নয়, একটি নিয়ম। ছোট একক, গোল করার রীতি আর সর্বোচ্চ মান, তিনটিই আগে ঠিক করুন। আর database-এর আগেই ভুল থামান।

প্র ০৩Code generation-এর ধাপটি কি দরকারি? Schema থেকে কোড বানানো, আর TypeScript-এই schema লেখা, এই দুই পথের পার্থক্য কী?

দুটি দর্শন আছে। Prisma-র পথ হলো আলাদা schema ভাষা: একটি নিজস্ব ফাইল, আর তা থেকে তৈরি কোড। Drizzle-এর মতো টুলের পথ হলো TypeScript-এই schema: table একটি সাধারণ object, আর type আসে TypeScript-এর inference থেকে। পর্ব ৩৭-এর tRPC-র সাথে তুলনা করলে বোঝা সহজ: সেখানেও চলছিল “generate করব, নাকি infer করব” এই প্রশ্ন।

Schema ভাষার সুবিধা: ফাইলটি পড়া সহজ, যাঁরা TypeScript জানেন না তাঁরাও বুঝতে পারেন, আর সব একই নিয়মে থাকে। Tool-গুলো (format, validate, migrate) এই একটি ফাইল ধরেই চলে। Generated type আগে থেকে লেখা থাকে বলে compiler-কে গভীর inference করতে হয় না।

দাম হলো একটি বাড়তি ধাপ। ভুলে গেলে client পুরোনো থাকে, যা আপনি weightGram-এর run-এ দেখেছেন। Prisma 7-এ db push আর migrate dev আর নিজে generate করে না, তাই ঝুঁকিটি স্পষ্ট হলো। Generated কোড Git-এ না থাকলে CI-তেও ধাপটি যোগ করতে হয়।

Inference-এর পথে বাড়তি ধাপ নেই, কোড বদলালেই type বদলায়। তবে table অনেক হলে compiler-এর ওপর চাপ বাড়তে পারে, আর schema-টি কিছুটা কোডের মতো পড়তে হয়। SQL-এর কাছাকাছি থাকাকে অনেকে সুবিধা মনে করেন, কেউ বোঝা মনে করেন। সমাজে এখানে একমত নেই, আর কোনোটিই সব প্রজেক্টে সেরা নয়।

আরেকটি প্রশ্ন, generated কোড Git-এ রাখবেন কি? init বাদ দেয়, কারণ এটি উৎপন্ন ফল, আর diff-এ শব্দ বাড়ায়। কেউ রাখেন যেন review-তে type-এর বদল দেখা যায়। পর্ব ৫-এর lockfile-এর সাথে মিলিয়ে ভাবুন: lockfile উৎপন্ন হলেও রাখা হয়, কারণ সে একটি সিদ্ধান্তের দলিল।

মূল উপলব্ধি: এক সত্যের জায়গা (single source of truth) ঠিক রাখাই আসল লক্ষ্য। Generate বা infer, দুটোই সেই একই কাজের দুই পথ, আর প্রতিটির খরচ ভিন্ন জায়গায় পড়ে।

প্র ০৪“আগে দেখি আছে কি না, না থাকলে বানাই” — এই সাধারণ যুক্তিটি কেন production-এ ভেঙে পড়ে? Database-এর constraint-এর ওপর এত ভরসা কেন?

মানুষের যুক্তিতে “দেখা” আর “করা” এক মুহূর্তের কাজ। সার্ভারে তা নয়। SELECT আর INSERT দুটি আলাদা query, আর মাঝে সময়ের ফাঁক আছে। পরিভাষায় একে check-then-act সমস্যা, বা TOCTOU (time of check to time of use) বলে। অপারেটিং সিস্টেমের ফাইল নিরাপত্তার আলোচনায় এই শব্দ ১৯৭০-এর দশক থেকেই পরিচিত।

আমাদের run-এই সংখ্যা দেখেছেন। ১০টি অনুরোধ একসাথে এলো, সবাই SELECT-এ দেখল সারি নেই, সবাই INSERT করতে গেল, একজন জিতল, ৯ জন P2002 পেল। ব্যবহারকারীর চোখে ৯টি ব্যর্থতা, আর stock-এর হিসাবে ৯টি ক্ষতি। Unique constraint না থাকলে অবস্থা আরও খারাপ হতো: কোনো error ছাড়াই ১০টি ডুপ্লিকেট সারি।

Application-এ নিয়ম লেখা কেন যথেষ্ট নয়? কারণ অ্যাপের একাধিক copy চলে, background job চলে, আর কেউ হাতে SQL চালান। শুধু database-ই সব লেখার একমাত্র মিলনস্থল। তাই unique, foreign key আর check constraint হলো শেষ পাহারা। পর্ব ৩৩-এর idempotency key-তেও এই একই চিন্তা ছিল।

ঠিক নকশা হলো তিন স্তর। প্রবেশপথে Zod আকার যাচাই করে, যাতে বন্ধুসুলভ বার্তা দেওয়া যায়। Database constraint সত্য রক্ষা করে। আর ভুলটি ধরে কেন্দ্রীয় handler 409-এ বদলায়। সুন্দর UX-এর জন্য আগে একটি SELECT করা যেতেই পারে, যেমন “এই নাম খালি কি?”, কিন্তু সেটি শুধু পরামর্শ, পাহারা নয়।

Native upsert এই কাজ এক statement-এ করে, তাই ১০টির ১০টিই সফল। কিন্তু সব শর্ত মিললে তবেই, নইলে Prisma আবার কয়েকটি query-তে ভাঙে, যা log-এ দেখেছেন। অনেক সারি আর হিসাব জড়ালে লাগে transaction, যা পর্ব ৪৩-এর বিষয়।

মূল উপলব্ধি: যে নিয়ম কখনো ভাঙা চলবে না, সেটি থাকবে database-এ। কোডের যাচাই ভদ্রতা, constraint হলো আইন।

প্র ০৫ORM-এর বানানো SQL কতটা বিশ্বাস করবেন? কখন client ছেড়ে নিজে SQL লিখবেন, আর তখন injection থেকে কীভাবে বাঁচবেন?

Query log খুললে Prisma-র SQL একটু অদ্ভুত লাগে। WHERE ("sku" = $1 AND 1=1), LIMIT $2 OFFSET $3, আর count-এর জন্য একটি subquery. Cursor-এ WHERE "id" >= (SELECT ...). এগুলো মানুষের হাতে লেখা SQL নয়, একটি সাধারণ যন্ত্রের ফল, যাকে সব ধরনের query সামলাতে হয়। বেশিরভাগ ক্ষেত্রে PostgreSQL-এর planner এগুলো সহজ করে নেয়, তাই অদ্ভুত দেখানো মানেই ধীর নয়। সিদ্ধান্ত হয় মেপে, পর্ব ৩৪-এর EXPLAIN দিয়ে, চেহারা দেখে নয়।

২০০৬ সালে Ted Neward ORM-কে “computer science-এর Vietnam” বলে একটি বিখ্যাত প্রবন্ধ লেখেন, আর Joel Spolsky-র “leaky abstraction” কথাটিও (২০০২) এখানে খাটে। দুটো কথার সারমর্ম একই: সহজ কাজ সহজ হয়, কিন্তু কঠিন কাজে নিচের স্তরটি জানতেই হয়। তাই SQL জানা ORM-এর বিকল্প নয়, পূর্বশর্ত।

কখন নিজে লিখবেন? জটিল রিপোর্ট, window function, ডেটাবেস-নির্দিষ্ট সুবিধা, বা মাপে যে query ধীর। তখন Prisma-র $queryRaw আছে। এটি একটি tagged template, তাই ভেতরের ${sku} নিজে থেকেই parameter হয়। আমাদের run-এর log-এ গেল WHERE sku = $1, আর মান আলাদা। বিপদ হলো $queryRawUnsafe বা নিজে স্ট্রিং জোড়া দেওয়া, যেখানে SQL injection ফিরে আসে। একটি ছোট ফাঁদ: সেখানে column-এর নাম parameter হতে পারে না, যা পর্ব ৩৪-এর ORDER BY সমস্যাই।

এক run-এ দেখা গেছে, pg_sleep-এর মতো void ফেরত দেওয়া কলাম $queryRaw পড়তে পারেনি, ::text cast লেগেছে। অর্থাৎ raw পথে type-এর দায়িত্বও আপনার।

মূল উপলব্ধি: ORM একটি সুবিধা, চুক্তি নয়। ৯০% কাজ client-এ রাখুন, log দেখুন, মাপুন। আর যেখানে SQL-ই সহজ, সেখানে parameter-সহ নিজে লিখুন।

অনুশীলন

হিসাব, সমালোচনা আর ফল অনুমান। শেষেরটি নিজের মেশিনে চালাতে হবে।

  1. হিসাব: (ক) ৳৮৯৯.৯৯ দামের একটি পণ্যের pricePaisa কত? ২৪০০০টি বিক্রি হলে মোট পয়সা কত, আর সেটি কি Int-এ ধরবে? (খ) Pool-এ ৫টি সংযোগ, ৪০টি query, প্রতিটি ৫০ ms. আদর্শ সময় কত? (গ) PostgreSQL-এর max_connections ১০০, আর ৪টি অ্যাপ server-এর প্রতিটিতে pool ৩০. সবাই পুরো ব্যবহার করলে কী হবে?

    (ক) ৮৯৯.৯৯ × ১০০ = 89999 পয়সা। মোট 89,999 × 24,000 = 2,159,976,000 পয়সা, অর্থাৎ ৳২,১৫,৯৯,৭৬০। এটি Int-এর সীমা 2,147,483,647 ছাড়িয়ে গেছে। তাই মোটের জন্য BigInt লাগবে। JavaScript-এর number-এ অবশ্য এটি নিরাপদ।

    (খ) ⌈40 ÷ 5⌉ × 50 = 8 × 50 = ৪০০ ms. বাস্তবে আরও কিছু বেশি, কারণ সংযোগ খোলার খরচ আছে।

    (গ) ৪ × ৩০ = ১২০ সংযোগ, সীমা ১০০. ২০টি সংযোগ 53300 too many clients পাবে, আর admin-এর জন্য সংরক্ষিত সংযোগ বাদ দিলে আরও আগেই। সমাধান: প্রতি server-এ pool কমানো (যেমন ২০), অথবা PgBouncer-এর মতো একটি connection pooler বসানো।

  2. নকশা সমালোচনা: AI একটি কোড দিল। কমপক্ষে ছয়টি সমস্যা ধরুন।
    // schema: model Product { id Int @id @default(autoincrement())
    //   sku String   price Float   stock Int }
    import { PrismaClient } from '@prisma/client';
    
    app.post('/products', async (req, res) => {
      const prisma = new PrismaClient();
      const found = await prisma.product.findFirst({ where: { sku: req.body.sku } });
      if (found) return res.status(400).send('exists');
      try {
        res.json(await prisma.product.create({ data: req.body }));
      } catch (e) {
        res.status(500).json({ error: e.message });
      }
    });
    • sku-তে @unique নেই: race-এ ডুপ্লিকেট চুপচাপ ঢুকবে।
    • price Float: দশমিকের ভুল। pricePaisa Int দরকার।
    • পুরোনো import আর adapter নেই: Prisma 7-এ চলবেই না।
    • প্রতি request-এ নতুন client: প্রতিবার নতুন pool, সংযোগ জমবে, বন্ধও হয় না।
    • findFirst, তারপর create: race. ডুপ্লিকেটের status-ও 400 নয়, 409.
    • data: req.body: যাচাই নেই, mass assignment-এর ঝুঁকি। req.body.sku undefined হলে findFirst যেকোনো একটি সারি পাবে, আর ভুলভাবে “exists” বলবে।
    • e.message বাইরে: এতে ফাইলের path আর কোডের লাইন থাকে। সব ভুলের জন্য 500-ও ভুল।
  3. ফল অনুমান: একটি খালি products টেবিল, id-র sequence ১ থেকে। sku unique. নিচের কাজগুলো পরপর হলে শেষে সারিগুলো আর r-এর মান কী?
    await prisma.product.create({ data: { sku: 'A', stock: 1, /*…*/ } });
    const r = await prisma.product.createMany({
      data: [{ sku: 'A', stock: 9 /*…*/ }, { sku: 'B', stock: 2 /*…*/ }],
      skipDuplicates: true,
    });
    await prisma.product.upsert({
      where: { sku: 'B' },
      create: { sku: 'B', stock: 100 /*…*/ },
      update: { stock: { increment: 3 } },
    });
    await prisma.product.upsert({
      where: { sku: 'C' },
      create: { sku: 'C', stock: 7 /*…*/ },
      update: { stock: { increment: 3 } },
    });

    r = { count: 1 }, কারণ A বাদ পড়ল।

    • id 1: A, stock 1. createMany একে ছোঁয়নি, তাই 9 বসেনি।
    • id 3: B, stock 2 + 3 = 5. create-এর 100 চলেনি।
    • id 5: C, stock 7.

    id-র হিসাব: A পেল 1. createMany-তে বাদ পড়া A খেল 2, B পেল 3. প্রথম upsert-এর সংঘাত খেল 4 (আসল run-এ ঠিক এভাবেই 8 নষ্ট হয়েছে). C পেল 5. একই ধরনের ক্রম lab-এ চলেছে, তবে id-র সংখ্যা sequence-এর আচরণ, এর ওপর নির্ভর করে কোনো যুক্তি লিখবেন না।

  4. কমান্ড: নিজের মেশিনে একটি PostgreSQL-এ Category model যোগ করুন, যাতে slug unique আর name থাকে। db push চালান, তারপর generate না করে prisma.category ব্যবহার করুন। কী দেখলেন? তারপর কোন কমান্ড সমাধান করবে, আর query log-এ findUnique-এর SQL কেমন হবে?

    Generate-এর আগে tsc বলবে prisma.category নেই (Property 'category' does not exist ধরনের বার্তা). কারণ table আছে, কিন্তু client পুরোনো। সমাধান npx prisma generate. তারপর findUnique({ where: { slug } })-এর SQL হবে SELECT … FROM "Category" WHERE ("slug" = $1 AND 1=1) LIMIT $2 OFFSET $3 ধরনের (নমুনা). @@map না দিলে table-এর নাম হবে "Category", বড় হাতের C-সহ, যা হাতে SQL লেখার সময় quote চায়। তাই @@map("categories") দেওয়া ভালো।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি নতুন project-এ প্রথম query পর্যন্ত ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. prisma, @prisma/client, adapter আর pg install
  2. prisma init দিয়ে schema আর config ফাইল তৈরি
  3. .env-এ DATABASE_URL আর schema-য় model লেখা
  4. Table বানাতে db push বা migrate চালানো
  5. prisma generate দিয়ে client তৈরি
  6. Adapter-সহ PrismaClient বানিয়ে query চালানো

Prisma 7-এ schema-য় একটি ঘর যোগ করে prisma db push চালানো হলো। কোডে নতুন ঘরটি ব্যবহার করতে TypeScript error দিচ্ছে। সবচেয়ে সম্ভাব্য কারণ কী?

  • Database-এ ঘরটি তৈরি হয়নি
  • prisma generate চালানো হয়নি, client পুরোনো
  • Node-এর version পুরোনো
  • Driver adapter দেওয়া হয়নি

৳১,২৫০.৭৫ দামের একটি পণ্য pricePaisa Int ঘরে রাখতে হবে। কোন মানটি ঠিক?

  • 1250.75
  • 1250
  • 125075
  • 1250750

Prisma-র update এমন একটি sku-তে চলল, যা টেবিলে নেই। কী হবে?

  • P2025 error ছোড়া হবে
  • null ফেরত আসবে
  • নতুন সারি তৈরি হবে
  • P2002 error ছোড়া হবে

৭টি সারির টেবিলে const id = undefined; রেখে prisma.product.findMany({ where: { sku: id } }) চলল। কী ফেরত আসার কথা?

  • খালি array
  • একটি validation error
  • শুধু যেসব সারিতে sku খালি
  • ৭টি সারিই

Node অ্যাপে API-র জন্য একটি পণ্য তৈরির সময়ে ডুপ্লিকেট SKU আটকানোর সবচেয়ে নির্ভরযোগ্য উপায় কোনটি?

  • Create-এর আগে findUnique করে দেখা
  • @unique রাখা, আর P2002-কে 409-এ বদলানো
  • Zod schema-য় sku বাধ্যতামূলক করা
  • Frontend-এ বোতাম বন্ধ করে দেওয়া

Next.js-এর dev server-এ কাজ করতে করতে কিছুক্ষণ পরে too many clients already আসছে। Prisma Client একটি module-এ new PrismaClient(...) দিয়ে বানানো। প্রথমে কী দেখবেন?

  • max_connections বাড়ানো
  • Query-গুলো ছোট করা
  • Client-কে globalThis-এ singleton করা
  • Prisma Studio দিয়ে table দেখা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Version আগে: npm view prisma dist-tags দেখুন, আর prisma ও @prisma/client একই stable version-এ রাখুন।
  2. Prisma 7: prisma-client generator, নিজের folder-এ client, driver adapter, আর URL config ফাইলে।
  3. Schema বদলালে table আর client, দুটোই আলাদা করে মেলাতে হয়। generate ভুলবেন না।
  4. টাকা Int paisa-তে, আর প্রবেশপথে Zod-এর .int(). Query log দেখে আসল SQL মেলান।
  5. P2002 → 409, P2025 → 404, undefined মানে শর্ত নেই, আর একটিই singleton client.

পরের পর্বে: db push ইতিহাস রাখে না। চলন্ত দোকানের table বদলাতে হলে কী করবেন? পর্ব ৪০-এ migration আর seed দিয়ে schema বদলের নিরাপদ পথ দেখবেন।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান