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

ORM-এর ধারণা — object আর table, query builder বনাম raw SQL

The ORM Idea — Objects and Tables, Query Builders vs Raw SQL

Taught in Bangla, this lesson explains the object-relational mismatch and three levels of database access, runs the same Bazarghar query with pg, Kysely, Drizzle and Prisma and compares the SQL they send, shows parameters versus string building in SQL injection, connection pools and serverless connection limits, Active Record versus Data Mapper, type safety, what ORMs hide and when to drop to raw SQL, measured overhead, and how to choose a tool.

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

  • Object আর table-এর অমিল ধরতে, আর একটি ছোট mapper নিজে পড়তে
  • একই query driver, query builder আর ORM-এ লিখে তৈরি হওয়া SQL মিলিয়ে দেখতে
  • Parameter আর injection-এর পার্থক্য বুঝতে, connection pool-এর আকার হিসাব করতে, আর serverless-এর ঝুঁকি চিনতে
  • Active Record আর Data Mapper আলাদা করতে, আর প্রতিটি tool-এর type safety-র সীমা জানতে
  • কখন raw SQL-এ নামবেন, আর প্রকল্পের জন্য tool ও ডেটাবেস কীভাবে বাছবেন, তা ঠিক করতে

১ · দুই জগতের অমিল — object আর table

বাজারঘরের API এতদিন চলেছে ছোট SQLite ফাইল আর হাতে লেখা query-তে। এবার সত্যিকারের ডেটাবেসের পালা। SQL কীভাবে লিখতে হয়, সেটি DBMS ও SQL কোর্সের কাজ, আর ORM-এর ভেতরের session ও unit of work আছে ORM আর migration পর্বে। এখানে আমাদের প্রশ্ন ব্যবহারিক: একটি TypeScript অ্যাপে কোন স্তরে কাজ করবেন, আর তার দাম কত।

আপনার কোডে একটি পণ্য হলো object: তার ভেতরে নাম, দাম, আর তার ক্যাটাগরি। ডেটাবেসে একই পণ্য products table-এর একটি সারি, আর ক্যাটাগরি আছে আলাদা table-এ, foreign key দিয়ে জোড়া। এই অমিলের নাম impedance mismatch.

দর্জির দোকানের কথা ভাবুন। ক্রেতা বলেন, “একটি নীল পাঞ্জাবি, রহিম ভাইয়ের মাপে।” দর্জির খাতায় কিন্তু সেটি তিন জায়গায় লেখা: মাপের খাতায় রহিম ভাইয়ের মাপ, কাপড়ের খাতায় নীল কাপড়ের হিসাব, অর্ডারের খাতায় শুধু দুটি নম্বর। মুখের কথা থেকে খাতায় লেখা, আর খাতা থেকে আবার পুরো পাঞ্জাবির ছবি, এই অনুবাদ রোজ করতে হয়। ORM হলো সেই অনুবাদের সহকারী।

কোডে (object)ডেটাবেসে (table)অনুবাদের প্রশ্ন
product.category.nameদুই table, foreign keyক্যাটাগরি কখন আনব, JOIN না আলাদা query?
categoryId (camelCase)category_id (snake_case)কে নাম মেলাবে?
product.stock = 30UPDATE না পাঠালে কিছুই বদলায় নাকোন ঘর বদলেছে, কে মনে রাখবে?
পরিচয় = মেমরির ঠিকানাপরিচয় = primary keyএকই সারি দুবার পড়লে দুটি object?
Date, numbertimestamptz, numeric, bigintType মেলে না, যেমন বড় সংখ্যা

নিচের ছোট প্রোগ্রামটি ORM-এর দুই দিকের কাজ হাতে করে। একটি JOIN-এর চ্যাপ্টা সারি থেকে object বানায়, আর object বদলালে শুধু বদলানো ঘরের UPDATE তৈরি করে। এটি শেখানোর মডেল, আসল ORM নয়।

// Flat rows from one JOIN: snake_case, the category repeats on every row.
const rows = [
  { id: 17, name: 'Chola dal', price: 120, stock: 12, category_id: 2, category_name: 'Dal' },
  { id: 1, name: 'Mosur dal', price: 145, stock: 0, category_id: 2, category_name: 'Dal' },
];

class Category { constructor(id, name) { this.id = id; this.name = name; } }
class Product {
  constructor(r, category) {
    Object.assign(this, { id: r.id, name: r.name, price: r.price, stock: r.stock, category });
  }
  inStock() { return this.stock > 0; }
}

function mapRows(list) {
  const seen = new Map();        // one Category object per primary key
  return list.map((r) => {
    if (!seen.has(r.category_id)) seen.set(r.category_id, new Category(r.category_id, r.category_name));
    return new Product(r, seen.get(r.category_id));
  });
}

function dirtySql(before, now) { // object back to a table: only what changed
  const changed = ['name', 'price', 'stock'].filter((k) => before[k] !== now[k]);
  const set = changed.map((k, i) => `${k} = $${i + 1}`).join(', ');
  return { sql: `UPDATE products SET ${set} WHERE id = $${changed.length + 1}`,
           params: [...changed.map((k) => now[k]), now.id] };
}

const [a, b] = mapRows(rows);
console.log(a.name, a.category.name, 'inStock:', a.inStock());
console.log('same Category object:', a.category === b.category);
const before = { ...b };
b.stock = 30;
console.log(dirtySql(before, b));

আউটপুট:

Chola dal Dal inStock: true
same Category object: true
{
  sql: 'UPDATE products SET stock = $1 WHERE id = $2',
  params: [ 30, 1 ]
}

সারিতে category_name দুবার এসেছিল, কিন্তু object হলো একটিই, তাই same Category object: true. একই primary key-তে একটি object রাখার এই ছকের নাম identity map. শুধু stock বদলেছে, তাই UPDATE-এ একটিই ঘর, আর 30 SQL-এর লেখায় নয়, params-এ। আসল ORM এই কাজের পাশে সম্পর্ক, transaction আর আরও অনেক ফাঁদ সামলায়।

২ · তিন স্তর — একই “বাজারঘর” query চারভাবে

অ্যাপ থেকে ডেটাবেসে যাওয়ার পথ মোটামুটি তিনটি স্তরে ভাগ করা যায়। সবচেয়ে নিচে database driver, যেমন pg. আপনি raw SQL লেখেন, driver পাঠায়। তার ওপরে query builder, যেমন Kysely. এটি কোডের method থেকে SQL বানায়। সবচেয়ে ওপরে ORM, যেমন Prisma: আপনি কী চান বলেন, সে SQL আর সম্পর্ক দুটিই সামলায়।

তিন স্তর, এক গন্তব্য

Every level ends as SQL text plus parameters

ORM, query builder আর raw SQL: সবাই SQL পাঠায় ORM: Prisma, Drizzle “ডালের পণ্য দিন, ক্যাটাগরিসহ” Query builder: Kysely .selectFrom().innerJoin().where() Raw SQL + driver: pg SELECT … JOIN … WHERE slug = $1 SQL লেখা + parameter সবার শেষ রূপ একই Connection pool PostgreSQL
ছবি ৩৮.১ — ওপরের স্তরে কম লিখতে হয়, কিন্তু কী যাচ্ছে তা কম দেখা যায়। নিচের সব ধাপ তিন স্তরেরই সমান।

এবার একটি বাস্তব কাজ: “ডাল ক্যাটাগরির, স্টকে আছে এমন, সবচেয়ে সস্তা ৩টি পণ্য, ক্যাটাগরির নামসহ।” ডেটাবেস আসল PostgreSQL 18.6, npm-এর embedded-postgres দিয়ে চালানো, যাতে কিছু install করতে না হয়। ১০,০০০টি পণ্য, ৮টি ক্যাটাগরি।

const r = await pool.query(
  `SELECT p.id, p.name, p.price, c.name AS category
     FROM products p JOIN categories c ON c.id = p.category_id
    WHERE c.slug = $1 AND p.stock > $2
    ORDER BY p.price, p.id LIMIT $3`, ['dal', 0, 3]);
const rows = await db.selectFrom('products as p')
  .innerJoin('categories as c', 'c.id', 'p.category_id')
  .select(['p.id', 'p.name', 'p.price', 'c.name as category'])
  .where('c.slug', '=', 'dal').where('p.stock', '>', 0)
  .orderBy('p.price').orderBy('p.id').limit(3).execute();
const rows = await db
  .select({ id: products.id, name: products.name,
            price: products.price, category: categories.name })
  .from(products)
  .innerJoin(categories, eq(categories.id, products.categoryId))
  .where(and(eq(categories.slug, 'dal'), gt(products.stock, 0)))
  .orderBy(asc(products.price), asc(products.id)).limit(3);
const rows = await prisma.product.findMany({
  where: { category: { slug: 'dal' }, stock: { gt: 0 } },
  select: { id: true, name: true, price: true,
            category: { select: { name: true } } },
  orderBy: [{ price: 'asc' }, { id: 'asc' }],
  take: 3,
});

চারটি একই প্রথম সারি দিল। Prisma-র ফলে শুধু ক্যাটাগরি object-এর ভেতরে, কারণ ORM সম্পর্ককে সম্পর্কের মতোই রাখে:

সত্যিকারের run · PostgreSQL 18.6 · ~/bazarghar · আউটপুট ছাঁটা, শেষের সংখ্যা মোট সারি · npm-এর latest তখন Prisma 8.0.0-rc.29, তাই stable 7.10.1 ব্যবহার হয়েছে

pg       {"id":17,"name":"ছোলার ডাল","price":120,"category":"ডাল"} 3
kysely   {"id":17,"name":"ছোলার ডাল","price":120,"category":"ডাল"} 3
drizzle  {"id":17,"name":"ছোলার ডাল","price":120,"category":"ডাল"} 3
prisma   {"id":17,"name":"ছোলার ডাল","price":120,"category":{"name":"ডাল"}} 3

খেসারি ডাল ১১০ টাকায় সবচেয়ে সস্তা হলেও আসেনি, কারণ তার স্টক ০. (এই তুলনার ল্যাবে সরলতার জন্য price পূর্ণ টাকায় রাখা হয়েছে। বাজারঘরের আসল চুক্তিতে টাকা পূর্ণসংখ্যা পয়সায়, পর্ব ৩২-এর মতো।) ফল এক, কিন্তু ডেটাবেসে কী গেল? প্রতিটি tool-এ log চালু করলে উত্তর মেলে। পড়ার সুবিধার জন্য লাইন ভেঙে ও ছেঁটে দেখানো হলো।

সত্যিকারের run · তিনটি tool-এর নিজের query log · লাইন ভাঙা ও ছাঁটা

kysely:  select "p"."id", "p"."name", "p"."price", "c"."name" as "category"
         from "products" as "p" inner join "categories" as "c" on "c"."id" = "p"."category_id"
         where "c"."slug" = $1 and "p"."stock" > $2 order by "p"."price", "p"."id" limit $3
         [ 'dal', 0, 3 ]

drizzle: select "products"."id", "products"."name", "products"."price", "categories"."name"
         from "products" inner join "categories" on "categories"."id" = "products"."category_id"
         where ("categories"."slug" = $1 and "products"."stock" > $2)
         order by "products"."price" asc, "products"."id" asc limit $3 -- params: ["dal", 0, 3]

prisma:  SELECT … "products"."id", … "products"."category_id" FROM "public"."products"
         LEFT JOIN "public"."categories" AS "j0" ON ("j0"."id") = ("public"."products"."category_id")
         WHERE (("j0"."slug" = $1 AND ("j0"."id" IS NOT NULL)) AND "public"."products"."stock" > $2)
         ORDER BY … LIMIT $3 OFFSET $4   ["dal",0,"3","0"]
prisma:  SELECT "public"."categories"."id", "public"."categories"."name"
         FROM "public"."categories" WHERE "public"."categories"."id" IN ($1,$2,$3) OFFSET $4   [2,2,2,"0"]

Kysely আর Drizzle-এর SQL প্রায় হাতে লেখা SQL-এর মতোই। Prisma 7.10.1 এখানে দুটি query পাঠাল: প্রথমটি পণ্য আনে, দ্বিতীয়টি ক্যাটাগরি, আর শেষে কোডে দুই ফল জোড়া দেওয়া হয়। IN-এ একই 2 তিনবার এসেছে, সেটিও log-এই দেখা গেল। এটি ভুল নয়, Prisma-র কৌশল। নিচের স্টেপারে এই যাত্রার প্রতিটি ধাপ দেখুন।

1const rows = await prisma.product.findMany({2  where: { category: { slug: 'dal' }, stock: { gt: 0 } },3  select: { id: true, name: true, price: true,4            category: { select: { name: true } } },5  orderBy: [{ price: 'asc' }, { id: 'asc' }], take: 3,6});7console.log(rows[0].category.name);
এই মুহূর্তের মান
ডেটাবেসে যাচ্ছে
parameter
হাতে আছে
  1. Call. আপনি একটি সাধারণ object দিলেন। এখনো কিছু যায়নি। Prisma-র query আসলে চলে await-এর সময়।
  2. পরিকল্পনা। Prisma আপনার object পড়ে SQL ঠিক করে। where-এর category.slug-এর জন্য JOIN, আর ফলের category-র জন্য আলাদা query. সে নিজে থেকে category_id-ও যোগ করে, জোড়া দিতে লাগবে বলে।
  3. প্রথম query. Pool থেকে সংযোগ নিয়ে SQL আর parameter আলাদাভাবে যায়। ফেরে ৩টি সারি, যাদের category_id ২.
  4. দ্বিতীয় query. প্রথম ফলের category_id নিয়ে IN (2, 2, 2). ফেরে ১টি সারি: ডাল।
  5. জোড়া। Node-এর ভেতরেই দুই ফল মেলে। category_id ফেলে দেওয়া হয়, কারণ আপনি চাননি। এই হিসাবে CPU খরচ হয়, যা ৮ নম্বর অংশে মাপা হবে।
  6. Object. আপনি পেলেন সাধারণ JavaScript object, class নয়। rows[0].category.name এখন ডাল। পুরো পথে SQL ছিল, শুধু আপনার চোখের আড়ালে।

Drizzle নিয়ে একটি কথা। ওপরে তার SQL-ধাঁচের API দেখলেন, যা query builder-এর খুব কাছে। তার একটি relational API-ও আছে (db.query.products.findMany), যা run-এ একটি LEFT JOIN LATERAL পাঠিয়েছে। তাই Drizzle-কে ORM বলা হবে না query builder, সেটি মতের ব্যাপার। স্তরগুলো কঠিন দেয়াল নয়, একটি ঢাল।

Query builder ভেতরে খুব জটিল কিছু নয়। নিচে একটি ছোট শেখানোর মডেল আছে, যা দেখায় মূল কাজ দুটি: নাম যাচাই করা, আর মান params-এ আলাদা রাখা।

// A teaching model of a query builder. Use a real one in real projects.
const OPS = new Set(['=', '>', '<', '>=', '<=']);
function id(name) {               // names cannot be parameters, so check them
  if (!/^[a-z_][a-z0-9_]*$/.test(name)) throw new Error(`bad name: ${name}`);
  return `"${name}"`;
}

class Query {
  constructor(state) { this.s = state; }
  with(patch) { return new Query({ ...this.s, ...patch }); } // never mutates
  select(...cols) { return this.with({ cols: cols.map(id) }); }
  where(col, op, value) {
    if (!OPS.has(op)) throw new Error(`bad operator: ${op}`);
    const n = this.s.params.length + 1;
    return this.with({
      wheres: [...this.s.wheres, `${id(col)} ${op} $${n}`],
      params: [...this.s.params, value],
    });
  }
  compile() {
    const { table, cols, wheres, params } = this.s;
    const where = wheres.length ? ` WHERE ${wheres.join(' AND ')}` : '';
    return { sql: `SELECT ${cols.join(', ')} FROM ${id(table)}${where}`, params };
  }
}
const from = (table) =>
  new Query({ table, cols: ['*'], wheres: [], params: [] });

const base = from('products').select('id', 'name').where('stock', '>', 0);
console.log(base.where('price', '<', 200).compile());
console.log(base.compile());
console.log(from('products').where('name', '=', "x' OR '1'='1").compile());
try { from('products').where('price; DROP', '=', 1); }
catch (e) { console.log('refused:', e.message); }

আউটপুট:

{
  sql: 'SELECT "id", "name" FROM "products" WHERE "stock" > $1 AND "price" < $2',
  params: [ 0, 200 ]
}
{
  sql: 'SELECT "id", "name" FROM "products" WHERE "stock" > $1',
  params: [ 0 ]
}
{
  sql: 'SELECT * FROM "products" WHERE "name" = $1',
  params: [ "x' OR '1'='1" ]
}
refused: bad name: price; DROP

দ্বিতীয় ফলে "price" নেই, যদিও base থেকেই আগে একটি শর্ত যোগ হয়েছিল। প্রতিটি method নতুন object ফেরায়, তাই একটি ভিত্তি query থেকে অনেক query বানানো যায়। বিপজ্জনক লেখাটি তৃতীয় ফলে পুরোটাই params-এ বসেছে, আর ভুল কলামের নাম আগেই ফেরত গেছে: refused: bad name: price; DROP.

৩ · Parameter আর SQL injection

ফর্মে খালি ঘর পূরণের কথা ভাবুন। ব্যাংকের চেকে “টাকার পরিমাণ” ঘরে যা-ই লেখা হোক, ব্যাংক সেটিকে পরিমাণ হিসেবেই পড়ে, নতুন নির্দেশ হিসেবে নয়। Parameterized query ঠিক এই ঘর। SQL-এর গঠন আগে যায়, মান আলাদা যায়। আর string জোড়া দেওয়া মানে নিজের হাতে পুরো চেকটিই ব্যবহারকারীকে লিখতে দেওয়া। তখন ঘটে SQL injection: ইনপুটের অংশ SQL-এর আদেশ হয়ে যায়।

প্রতিরক্ষার দিক থেকে ব্যাপারটি বোঝার জন্য নিচে একটি নকল executor আছে, কোনো আসল ডেটাবেস নয়। এটি দেখায়, ডেটাবেস লেখাটিকে টুকরো করার পরে কী দেখে।

// A mock executor, not a database. It shows how text becomes structure.
// It understands only: SELECT slug FROM t WHERE a = b [OR a = b]
const table = ['chal', 'dal', 'tel', 'mosla'];

function tokenize(sql) {
  return sql.match(/'[^']*'|\$\d+|[A-Za-z]+|=/g);
}

function run(sql, params = []) {
  const tokens = tokenize(sql);
  const clause = tokens.slice(tokens.indexOf('WHERE') + 1);
  const value = (t, slug) => t === 'slug' ? slug
    : t.startsWith('$') ? params[Number(t.slice(1)) - 1]
    : t.slice(1, -1);                     // a quoted literal
  const rows = table.filter((slug) => {
    for (let i = 0; i < clause.length; i += 4) {
      if (value(clause[i], slug) === value(clause[i + 2], slug)) return true;
    }
    return false;
  });
  return { clause: clause.join(' '), rows };
}

const input = "x' OR '1'='1";

console.log('concatenated:', run(`SELECT slug FROM t WHERE slug = '${input}'`));
console.log('parameter   :', run('SELECT slug FROM t WHERE slug = $1', [input]));
console.log('normal value:', run('SELECT slug FROM t WHERE slug = $1', ['dal']));

আউটপুট:

concatenated: {
  clause: "slug = 'x' OR '1' = '1'",
  rows: [ 'chal', 'dal', 'tel', 'mosla' ]
}
parameter   : { clause: 'slug = $1', rows: [] }
normal value: { clause: 'slug = $1', rows: [ 'dal' ] }

প্রথম লাইনে ইনপুটের ' লেখাটির সীমা ভেঙে দিয়েছে, তাই শর্ত হলো দুটি, আর '1' = '1' সবসময় সত্য। দ্বিতীয় লাইনে শর্তের গঠন বদলায়নি। পুরো লেখাটি একটি মান, যা কোনো slug-এর সাথে মেলে না, তাই rows: []. তৃতীয় লাইন দেখায় স্বাভাবিক কাজ ঠিকই চলে।

একই ঘটনা আসল PostgreSQL-এ, শুধু আমাদের ৮টি ক্যাটাগরির সাধারণ table-এ:

সত্যিকারের run · PostgreSQL 18.6 · pg 8.23.1 · local ল্যাব · ইনপুট x' OR '1'='1

concatenated: { n: 8 }
parameter   : { n: 0 }

Query builder আর ORM-এ মান সাধারণভাবে নিজে থেকেই parameter-এ যায়, এটি তাদের বড় সুবিধা। কিন্তু দুটি জায়গায় সুরক্ষা থাকে না। প্রথমত, কলাম বা table-এর নাম parameter হতে পারে না, যা পর্ব ৩৪-এর ORDER BY ফাঁদে দেখেছেন। দ্বিতীয়ত, raw SQL-এর “unsafe” পথে। আক্রমণের ধরন আর পূর্ণ প্রতিরক্ষা আছে Injection প্রতিরক্ষা পর্বে। এখানে একটি নিয়মই যথেষ্ট: মান parameter-এ, নাম allowlist থেকে।

৪ · Connection pool — ধার করা সংযোগ

ডেটাবেসের সংযোগ বানানো দামি। PostgreSQL প্রতিটি সংযোগের জন্য আলাদা একটি process চালায়, আর শুরুতে পরিচয় যাচাই লাগে। একই কম্পিউটারে একটি run-এ মাপা ফল:

সত্যিকারের run · একটি local কম্পিউটারে একবার · SELECT 1-এর মধ্যমা · 127.0.0.1

new connection each time: 21.32 ms   pooled: 0.054 ms

প্রায় ৪০০ গুণ পার্থক্য, আর এখানে network-ই নেই। তাই অ্যাপ connection pool রাখে। রেস্তোরাঁর কথা ভাবুন: প্রতিটি অর্ডারের জন্য নতুন ওয়েটার নিয়োগ করলে সময় নষ্ট হয়। পাঁচজন ওয়েটার থাকেন, অর্ডার নেন, কাজ শেষে আবার ফাঁকা হন। সবাই ব্যস্ত থাকলে নতুন অর্ডার সারিতে দাঁড়ায়। pg-এর Pool-এ ডিফল্ট max ১০. Prisma 7-এর pg adapter-ও একই pool ব্যবহার করে, আর সেখানেও ডিফল্ট ১০.

Prisma 6 থেকে আসলে সাবধান। আগে সীমা দেওয়া হতো URL-এর connection_limit দিয়ে। Prisma-র docs বলছে, version 7-এ pg adapter-এ সেটি max, আর connectionTimeoutMillis-এর ডিফল্ট ০, মানে অপেক্ষার কোনো সীমা নেই। Production-এ এই timeout নিজে দিন, নইলে আটকে যাওয়া request চুপচাপ ঝুলে থাকতে পারে।

এবার চাপ দেওয়া যাক। ২টি সংযোগের pool, আর একসাথে এলো ৫টি request, প্রতিটির query ১০০ ms.

সংযোগ দুটির অবস্থাপ্রতিটি query ১০০ ms

            
Pool-এর হিসাব
totalCount
idleCount
waitingCount
  1. শুরু। A আর B সংযোগ পেল। C, D, E সারিতে, তাই waitingCount ৩. ডেটাবেস ধীর নয়, ওরা শুধু অপেক্ষায় আছে।
  2. ১০০ ms. A আর B শেষ করে সংযোগ ফেরত দিল। C আর D নিল। E এখনো অপেক্ষায়, সে এরই মধ্যে ১০০ ms হারিয়েছে।
  3. ২০০ ms. E সংযোগ পেল, আর একটি সংযোগ ফাঁকা, তাই idleCount ১.
  4. ৩০০ ms. সব শেষ। E নিজের ১০০ ms-এর কাজে মোট ৩০০ ms লাগাল। সূত্র: ⌈৫ / ২⌉ × ১০০ = ৩০০ ms.
  5. Idle timeout. pg-এর ডিফল্ট idleTimeoutMillis ১০ সেকেন্ড। ততক্ষণ কেউ না এলে pool সংযোগ বন্ধ করে দেয়, তাই totalCount ০.
  6. Leak, ভিন্ন একটি গল্প। ধরুন A client.release() ভুলে গেল। তার সংযোগ আর ফেরে না। এটিকে connection leak বলে। এমন কয়েকবার হলেই সব request ঝুলে থাকবে, যদিও ডেটাবেস একদম ফাঁকা।

এখন আসল সংখ্যা। ২০টি query, প্রতিটি pg_sleep(0.1), একসাথে পাঠানো। হিসাবের সূত্র ⌈N / max⌉ × s, যেখানে N query-র সংখ্যা, s একটির সময়।

max৫০ ms-এ waitingসূত্রে (ms)মাপা মোট (ms)সবচেয়ে লম্বা অপেক্ষা (ms)
119200022082100
515400469358
1010200264154
20010018575

সত্যিকারের run · একটি local কম্পিউটারে একবার · pg 8.23.1 · ঘুমের query, তাই ডেটাবেসে আসল চাপ নেই

মাপা সময় সূত্রের চেয়ে বেশি, কারণ সংযোগ খুলতেও সময় লাগে আর প্রতি query-তে ছোট বাড়তি খরচ আছে। max=20-এ অপেক্ষার ৭৫ ms পুরোটাই একসাথে ২০টি সংযোগ খোলার সময়। তাহলে কি pool বড় রাখাই ভালো? এখানে হ্যাঁ, কারণ pg_sleep CPU খায় না। আসল query CPU আর disk নিয়ে লড়ে, তাই একটি সীমার পরে বেশি সংযোগ গতি কমায়। ২ নম্বর ভাবনার প্রশ্নে এর হিসাব আছে।

Pool-এর দুটি error চিনে রাখুন। দুটিই run থেকে:

সত্যিকারের run · max: 2, connectionTimeoutMillis: 300 · দুটি connect(), কোনো release() নেই

after two connect() with no release: total=2 idle=0
error: timeout exceeded when trying to connect

এই বার্তা দেখে অনেকে ভাবেন ডেটাবেস বন্ধ। আসলে এটি pool-এর সারিতে অপেক্ষার timeout. আগে দেখুন কোথাও release() বাদ পড়েছে কি না। pool.query() নিজেই নেয় আর ফেরত দেয়, তাই সেটিই সহজ পথ। pool.connect() লিখলে finally-তে release() রাখুন।

Serverless কেন pool ফুরিয়ে ফেলে

একটি Express server-এ একটি pool, সংখ্যাটি আপনার হাতে। Serverless-এ ভিড় বাড়লে platform কোডের অনেক কপি চালায়। প্রতিটি কপিতে আলাদা Node process, তাই আলাদা pool. Prisma-র docs-ও বলছে, প্রতিটি function নিজের client আর নিজের pool রাখে। ডেটাবেসের সীমা কিন্তু একটিই। নিচে ২৫টি পর্যন্ত “কপি” আলাদা pool দিয়ে নকল করা হলো, প্রতিটি max: 5. PostgreSQL-এর ডিফল্ট max_connections ১০০.

সত্যিকারের run · একটি local কম্পিউটারে একবার · প্রতিটি কপিতে আলাদা Pool, একই process-এ নকল করা · error-এর লাইন আলাদা করে সাজানো

max_connections 100  superuser_reserved 3
instances= 5 pool=5 wanted= 25 ok= 25 failed=  0 open-after=26
instances=10 pool=5 wanted= 50 ok= 50 failed=  0 open-after=51
instances=15 pool=5 wanted= 75 ok= 75 failed=  0 open-after=76
instances=25 pool=5 wanted=125 ok= 99 failed= 26 open-after=100
53300 sorry, too many clients already

১২৫টি চাওয়া, ৯৯টি পেল, ১টি ছিল মাপার নিজের সংযোগ, মোট ১০০. বাকি ২৬টি query সরাসরি ব্যর্থ। এখানে superuser দিয়ে চালানো হয়েছে। সাধারণ user হলে সংরক্ষিত ৩টি বাদে ৯৭টিতেই থামত। লক্ষ করুন open-after: কাজ শেষেও সংযোগ খোলা থাকে। Serverless কপি ঘুমিয়ে থাকলেও তার সংযোগ কিছু সময় জায়গা আটকে রাখতে পারে।

সরাসরি সংযোগ বনাম মাঝে একটি pooler

ওপরের সংখ্যা মাপা, নিচেরটি একটি নকশার উদাহরণ, run করা হয়নি

Serverless-এ সংযোগ: সরাসরি আর pooler-এর মাধ্যমে সরাসরি: ২৫ × ৫ = ১২৫ চাওয়া ২৫টি functionপ্রতিটি pool ৫ ১২৫ ডেটাবেসসীমা ১০০ ৯৯ চলল, ২৬ ব্যর্থ মাঝে pooler: প্রতিটি function ১টি ২৫টি functionপ্রতিটি ১টি ডেটাবেসঅল্প কয়েকটি Pooler: ধরুন ২০টি আসল সংযোগ ভাগ করে
ছবি ৩৮.২ — Pooler অনেক client নেয়, কিন্তু ডেটাবেসে অল্প সংযোগ রাখে। ২০ সংখ্যাটি শুধু উদাহরণ।

সমাধান সাধারণত তিন ধরনের। প্রতিটি কপিতে pool ছোট রাখা (max ১ বা ২, সংখ্যাটি বিতর্কিত, নিজের চাপে মেপে নিন)। মাঝে একটি connection pooler রাখা, যেমন PgBouncer, যা Prisma-র docs-ও serverless-এর জন্য বলে। অথবা provider-এর HTTP-ভিত্তিক driver, যদি থাকে। আরেকটি সহজ ভুল: request handler-এর ভেতরে new PrismaClient() লেখা। এতে প্রতি request-এ একটি নতুন pool হয়। Client একবার module-এর ওপরের স্তরে বানান। পর্ব ২৯-এর repository স্তরে এটি রাখা সহজ।

৫ · Active Record বনাম Data Mapper

ORM-এর দুটি বড় নকশার নাম আসে Martin Fowler-এর ২০০২ সালের Patterns of Enterprise Application Architecture বই থেকে। তত্ত্বটি ORM আর migration পর্বে আছে। মূল প্রশ্ন একটিই: ডেটাবেসে কে লিখবে? চায়ের দোকানে হিসাব দোকানি নিজের হাতে রাখেন, নাকি আলাদা একজন হিসাবরক্ষক?

Active Record

Object নিজেই নিজেকে সেভ করে: c.save(). দোকানি নিজের খাতা নিজে লেখেন। Rails, Laravel-এর Eloquent এই ধারায়। শুরুতে খুব সহজ, তবে ব্যবসার নিয়ম ও ডেটাবেস এক class-এ মিশে যায়।

Data Mapper

Object ডেটাবেস চেনে না, আলাদা স্তর লেখে: repo.save(c). দোকানি শুধু বিক্রি করেন, খাতা অন্যের হাতে। Hibernate, SQLAlchemy এই ধারায়। ডেটাবেস ছাড়াই নিয়ম পরীক্ষা করা সহজ।

TypeORM দুটিকেই সমর্থন করে, তাই পাশাপাশি দেখা সহজ। TypeScript-এর পুরোনো ও পরিচিত এই ORM-টি এখানে শুধু তুলনার জন্য। লেখার সময়ে ১.১.২ দিয়ে চালানো:

// Active Record: the object saves itself
const c = await Category.findOneByOrFail({ slug: 'dal' });
c.name = 'ডাল ও শস্য';
await c.save();

// Data Mapper: a repository saves the object
const repo = dataSource.getRepository(Category);
const d = await repo.findOneByOrFail({ slug: 'dal' });
d.name = 'ডাল';
await repo.save(d);

সত্যিকারের run · TypeORM 1.1.2 · PostgreSQL 18.6 · c.save()-এর SQL, ছাঁটা · repo.save(d)-তেও একই গঠন এসেছে

SELECT "Category"."id" AS "Category_id", … FROM "categories" "Category" WHERE "Category"."id" = $1  [2]
START TRANSACTION
UPDATE "categories" SET "name" = $1 WHERE "id" = $2  ["ডাল ও শস্য",2]
COMMIT

দুই পথের SQL হুবহু এক ধরনের। পার্থক্যটি কোডের গঠনে, ডেটাবেসে নয়। আর দেখুন, save() শুধু একটি UPDATE নয়: আগে একটি বাড়তি SELECT, তারপর transaction. একটি ছোট শব্দের পেছনে চারটি বিনিময়। লুপে ১,০০০ বার save() ডাকলে এর দাম কত হবে, ভাবুন।

Prisma কোন দিকে? Prisma-র docs নিজেকে Data Mapper ধারার বলে, তবে সাধারণ ORM থেকে আলাদা: কোনো entity class নেই, ফল সাধারণ object, আর সব কাজ prisma.product-এর মতো client দিয়ে। পর্ব ৬৮-তে Django আর পর্ব ৭০-এ Laravel-এর Eloquent-এ Active Record ধাঁচ দেখবেন। ছোট CRUD অ্যাপে Active Record দ্রুত। নিয়ম-ভারী ব্যবসায় Data Mapper পরে কম কষ্ট দেয়। কোনোটিই সর্বজয়ী নয়।

৬ · Type safety — কে কী দেয়

TypeScript-এর আসল সুবিধা হলো ভুল চালানোর আগেই ধরা (পর্ব ০৬). কিন্তু ডেটাবেস TypeScript-এর বাইরের জগৎ। প্রতিটি tool এই দূরত্ব ভিন্নভাবে পূরণ করে। একটি ফাইলে ইচ্ছা করে একই ভুল করা হলো: price-এর জায়গায় pirce.

// pg, no generic: rows are any
const a = await pool.query('SELECT id, price FROM products');
console.log(a.rows[0].pirce);                                   // line 15

// pg, with a generic: we promise a shape, nothing checks it
const b = await pool.query<{ id: number; discount: number }>(
  'SELECT id, price FROM products');
console.log(b.rows[0].discount);                                // line 19

await k.selectFrom('products').select('pirce').execute();       // line 22
await k.selectFrom('products').selectAll()
  .where('price', '=', 'cheap').execute();                      // line 25
await d.select().from(products).where(eq(products.pirce, 100)); // line 28
await prisma.product.findMany({ where: { pirce: 100 } });       // line 31

const row = await prisma.product.findFirstOrThrow({ select: { id: true, name: true } });
console.log(row.price);                                         // line 35

সত্যিকারের run · tsc 6.0.3, strict · বার্তা ছাঁটা

t.ts(22,39): error TS2769: No overload matches this call.
  … Argument of type '"pirce"' is not assignable to parameter of type 'SelectExpression<DB, "products">'.
t.ts(25,64): error TS2345: Argument of type '"cheap"' is not assignable to parameter of type
  'OperandValueExpressionOrList<DB, "products", "price">'.
t.ts(28,51): error TS2551: Property 'pirce' does not exist on type 'PgTableWithColumns<…>'. Did you mean 'price'?
t.ts(31,42): error TS2561: Object literal may only specify known properties, but 'pirce' does not
  exist in type 'ProductWhereInput'. Did you mean to write 'price'?
t.ts(35,17): error TS2339: Property 'price' does not exist on type '{ id: number; name: string; }'.

লাইন ১৫ আর ১৯-এর কোনো error নেই। চালালে দুটিই নীরবে undefined. Run-এ এসেছে typo at runtime: undefined. প্রথমটি any. দ্বিতীয়টি আরও সূক্ষ্ম: { discount: number } একটি প্রতিশ্রুতি, যাচাই নয়। এই ধরনের type assertion-এ compiler আপনাকে বিশ্বাস করে। আর লাইন ৩৫ সবচেয়ে সুন্দর: Prisma select দেখে ফলের type-ও ছোট করে নিল।

ToolType কোথা থেকেকলামের ভুল ধরেফলের আকার
pgনেই, বা আপনার দেওয়া genericনাany, বা আপনার প্রতিশ্রুতি
Kyselyআপনার লেখা interface, বা ডেটাবেস থেকে তৈরিহ্যাঁselect অনুযায়ী
DrizzleTypeScript-এ লেখা schemaহ্যাঁselect অনুযায়ী
Prismaschema.prisma থেকে prisma generateহ্যাঁselect অনুযায়ী

Type শুধু compile-এর সময়ের প্রতিশ্রুতি। Type চালানোর সময় মুছে যায়। ডেটাবেসের আসল গঠন কোডের ধারণা থেকে সরে গেলে, যাকে বলে schema drift, compiler কিছুই জানে না। ল্যাবে stock কলামের নাম বদলে আগের client চালানো হলো। tsc খুশি ছিল, কিন্তু চালাতে গিয়ে এলো: P2022 The column `products.stock` does not exist in the current database. তাই পর্ব ৪০-এর migration-এর শৃঙ্খলা আর API-র সীমানায় পর্ব ২৭-এর Zod যাচাই, দুটিই লাগে।

৭ · ORM যা লুকায়, আর কখন raw SQL-এ নামবেন

ORM-এর আরাম আর বিপদ একই জায়গায়: SQL চোখের আড়ালে। তাই নিয়ম হলো, development-এ log চালু রাখুন। Prisma-য় এটি কয়েক লাইনের কাজ:

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

export const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL, max: 10 }),
  log: [{ emit: 'event', level: 'query' }],
});

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

এভাবেই ২ নম্বর অংশের দুটি query ধরা পড়েছে। Prisma 7.10.1-এ relationJoins নামের preview feature চালু করে relationLoadStrategy: 'join' লিখলে একই কাজ একটি query-তে হয়েছে:

সত্যিকারের run · Prisma 7.10.1, relationJoins preview · ছাঁটা · নতুন version-এ এই আচরণ বদলাতে পারে, নিজের version-এর docs দেখুন

SELECT "t0"."id", "t0"."name", "t0"."price", "t0"."category_id" AS "categoryId",
       "Product_category"."__prisma_data__" AS "category"
FROM "public"."products" AS "t0"
LEFT JOIN "public"."categories" AS "j1" ON ("j1"."id") = ("t0"."category_id")
LEFT JOIN LATERAL (SELECT JSONB_BUILD_OBJECT('name', "t2"."name") AS "__prisma_data__" …
WHERE … LIMIT $4   ["1","dal",0,"3"]

ORM যে SQL পাঠায়, আপনি নিজেও সেটি পড়তে ও চালাতে পারেন। নিচের live ব্লকে একই কাজ SQLite-এর বানানে: Prisma-র দুই ধাপ, তারপর হাতের একটি JOIN, আর শেষে তার plan. Query বদলে দেখতে পারেন।

-- Step 1: the products (what Prisma's first query does)
SELECT p.id, p.name, p.price, p.category_id
FROM products p
LEFT JOIN categories j0 ON j0.id = p.category_id
WHERE j0.slug = 'dal' AND p.stock > 0
ORDER BY p.price, p.id LIMIT 3 OFFSET 0;

-- Step 2: the categories of those rows
SELECT id, name FROM categories WHERE id IN (2, 2, 2);

-- By hand: one JOIN gives everything at once
SELECT p.id, p.name, p.price, c.name AS category
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE c.slug = 'dal' AND p.stock > 0
ORDER BY p.price, p.id LIMIT 3;

EXPLAIN QUERY PLAN
SELECT p.id, p.name, p.price, c.name AS category
FROM products p
JOIN categories c ON c.id = p.category_id
WHERE c.slug = 'dal' AND p.stock > 0
ORDER BY p.price, p.id LIMIT 3;

প্রথম ফলে তিনটি সারি: 17, 1, 9, সবার category_id ২. দ্বিতীয় ফলে একটি সারি: 2 Dal, কারণ IN (2, 2, 2)-এ একই মান তিনবার থাকলেও সারি একবারই আসে। তৃতীয় ফলে একই পণ্য, এবার ক্যাটাগরির নামসহ, এক query-তে। Plan-এ idx_products_cat_price দেখুন: index-এ ক্রম আগে থেকেই আছে, তাই আলাদা sort লাগেনি। Plan পড়ার নিয়ম পর্ব ৩৪-এ দেখেছেন।

কখন raw SQL

ORM-এ সব কাজ করতে হবে, এমন কোনো নিয়ম নেই। ধরুন বাজারঘরের মালিক চান, “paid অর্ডারে প্রতিটি ক্যাটাগরির সবচেয়ে বেশি বিক্রি হওয়া পণ্য।” এখানে window function লাগে, যা Prisma-র সাধারণ API-তে নেই। তখন $queryRaw:

const status = 'paid', limit = 3;
const rows = await prisma.$queryRaw<
  { category: string; name: string; sold: bigint }[]
>`
  SELECT * FROM (
    SELECT c.name AS category, p.name, SUM(i.qty) AS sold,
           ROW_NUMBER() OVER (PARTITION BY c.id
                              ORDER BY SUM(i.qty) DESC, p.id) AS rnk
    FROM order_items i
    JOIN orders o ON o.id = i.order_id
    JOIN products p ON p.id = i.product_id
    JOIN categories c ON c.id = p.category_id
    WHERE o.status = ${status}
    GROUP BY c.id, c.name, p.id, p.name
  ) t WHERE rnk = 1 ORDER BY sold DESC LIMIT ${limit}`;

এটি একটি tagged template. দেখতে string জোড়ার মতো, কিন্তু ${status} SQL-এ ঢোকে না। ফাংশনটি লেখার টুকরো আর মান আলাদা পায়, আর মান যায় parameter হিসেবে। Log-ই তার প্রমাণ:

সত্যিকারের run · Prisma 7.10.1 · ছাঁটা · শেষের তিনটি ফলের পাশে “←” দিয়ে তাদের log সাজিয়ে দেওয়া হয়েছে

sql: SELECT * FROM ( SELECT c.name AS category, … WHERE o.status = $1 GROUP BY … LIMIT $2  ["paid",3]
[
  { category: 'চাল', name: 'Product 16', sold: 8n, rnk: 1n },
  { category: 'চিনি', name: 'Product 4', sold: 8n, rnk: 1n },
  { category: 'মসলা', name: 'Product 11', sold: 6n, rnk: 1n }
]
JSON: Do not know how to serialize a BigInt

tagged template: [ { n: 0 } ]    ← … WHERE slug = $1  ["x' OR '1'='1"]
Unsafe + concat: [ { n: 8 } ]    ← … WHERE slug = 'x' OR '1'='1'  []

order by param: [ { id: 8 }, { id: 16 } ]    ← … ORDER BY $1 DESC  ["price"]

চারটি শিক্ষা। এক, SUM আর COUNT এখানে BigInt হয়ে এলো: 8n. তাই res.json(rows) সোজা ভেঙে পড়ে। ::int cast করুন, বা উত্তর পাঠানোর আগে Number() দিন। দুই, <{…}[]> আবার একটি প্রতিশ্রুতি, যাচাই নয়। Prisma-র TypedSQL বা Zod যাচাই এই ফাঁক কমায়। তিন, tagged template আর $queryRawUnsafe-এর ফল ০ আর ৮. নামে “Unsafe” থাকাটা ফাঁকা কথা নয়। চার, ORDER BY ${col} কোনো error দেয়নি, শুধু sort করেনি, কারণ পর্ব ৩৪-এর মতোই এটি একটি মান।

Kysely-র sql আর Drizzle-এর sql tag-ও একইভাবে কাজ করে। তিনটি বিপজ্জনক অভ্যাস মনে রাখুন: $queryRawUnsafe-এ string জোড়া, বাইরে থেকে আসা লেখা Prisma.raw-এ দেওয়া, আর আগে string বানিয়ে পরে tagged template-এর ভেতরে বসানো। Prisma-র docs তিনটিতেই সতর্ক করে।

৮ · খরচ কত — মাপা overhead

“ORM ধীর”, এই কথা প্রায়ই শোনা যায়। কত ধীর, কোথায়? এবার মাপা যাক। প্রতিটি tool একই কম্পিউটারে, একই PostgreSQL-এ, ৫০০ বার গরম করে, তারপর তিন দফায় মোট ৯,০০০ বার চালানো হলো, এক সময়ে একটি করে।

ToolPrimary key: মধ্যমাp99গুণ২০ সারির তালিকা: মধ্যমাp99গুণ
pg0.0660.2221.000.1230.3261.00
Kysely0.0760.2331.140.1680.3871.37
Drizzle0.1180.2931.770.2020.4471.64
Prisma0.2260.4453.400.3430.7122.80
Prisma, join preview———0.2730.5602.23

সময় ms-এ · সত্যিকারের run, একটি local কম্পিউটারে একবার · 127.0.0.1 · Node.js 24.15 · ১০,০০০ সারি · মেমরিতে থাকা ডেটা · এটি সাধারণ ফল নয়, tool-এর version বদলালে সংখ্যা বদলাবে

“৩.৪ গুণ ধীর” শুনতে ভয়ংকর। কিন্তু যোগ করে দেখুন। Primary key query-তে বাড়তি সময় ০.২২৬ − ০.০৬৬ = ০.১৬ ms. ধরুন একটি পাতায় ৫টি query, আর পুরো অনুরোধের বাজেট ২০০ ms. তাহলে বাড়তি ৫ × ০.১৬ = ০.৮ ms, মানে বাজেটের ০.৪%. আর বাস্তবে ডেটাবেস অন্য কম্পিউটারে থাকলে প্রতি query-তে network-ই সাধারণত এর চেয়ে অনেক বেশি নেয়, যা এই run-এ নেই।

তবে একটি জায়গায় এই সংখ্যা গুরুত্বপূর্ণ। এই বাড়তি সময় Node-এর CPU-তে যায়, তাই event loop ব্যস্ত থাকে। এক সময়ে একটি করে চালালে ০.০৬৬ ms মানে সেকেন্ডে প্রায় ১৫,১৫০টি, আর ০.২২৬ ms মানে প্রায় ৪,৪২০টি। খুব বেশি query-র গরম পথে এটি টের পাওয়া যেতে পারে। বেশিরভাগ বাজারঘর সেখানে পৌঁছানোর অনেক আগে অন্য সমস্যায় পড়বে।

আসল বড় পার্থক্য আসে ব্যবহারের ধরনে। একই ১,০০০টি পণ্য ঢোকানো:

সত্যিকারের run · একটি local কম্পিউটারে একবার · ৫ বারের মধ্যমা · ১,০০০ সারি

pg unnest                7.0 ms
prisma createMany       54.3 ms
prisma create x1000    340.5 ms

Tool এক, কিন্তু লুপে create ডাকলে createMany-এর চেয়ে প্রায় ৬.৩ গুণ সময় লাগল। ORM-এর গতি নিয়ে তর্কের চেয়ে এই ধরনের ভুল বেশি ক্ষতি করে। N+1-ও একই জাতের সমস্যা, পর্ব ৪২-এ সেটি আসছে। আগে query-র সংখ্যা ঠিক করুন, তারপর tool নিয়ে ভাবুন।

৯ · কোনটি বেছে নেবেন — tool আর ডেটাবেস

এই তর্কে সম্প্রদায় সত্যিই বিভক্ত। কেউ বলেন SQL শিখুন, ORM বাদ দিন। কেউ বলেন CRUD হাতে লেখা সময়ের অপচয়। দুই পক্ষই নিজের প্রকল্পে সঠিক হতে পারেন। তাই নিয়ম নয়, প্রশ্ন দিয়ে এগোন।

পরিস্থিতিভালো শুরুকারণ
CRUD-ভারী অ্যাপ, ছোট দলORMদ্রুত, type আর সম্পর্ক তৈরি পাওয়া যায়
দলে SQL-এর ভালো দখল, query জটিলQuery builder বা DrizzleSQL দেখা যায়, type-ও থাকে
অল্প কয়েকটি query, ছোট servicepgDependency কম, কিন্তু type নিজের দায়িত্ব
রিপোর্ট, window function, CTERaw SQL, যে tool-ই হোকORM-এর সাথে লড়াই করার দরকার নেই
Serverless, ছোট bundle দরকারBundle মেপে দেখুনTool অনুযায়ী আকার ও সংযোগের পথ বদলায়

বাস্তবে অনেক দল মিশিয়ে কাজ করে: সাধারণ কাজে ORM, রিপোর্টে raw SQL. আমরা বাজারঘরে পর্ব ৩৯ থেকে Prisma ব্যবহার করব, কারণ শেখার পথে তার schema আর migration খুব স্পষ্ট। এর মানে এই নয় যে Prisma সবার সেরা। এখন একটি query ধরে সিদ্ধান্তের ধাপগুলো দেখুন।

কাজ: প্রতি ক্যাটাগরির সেরা-বিক্রীত পণ্যপ্রকল্পে Prisma আছে

            
বাছাইয়ের অবস্থা
ORM API
Query builder
Raw SQL
  1. আগে সরল প্রশ্ন। কাজটি একটি table-এর সাধারণ CRUD হলে ORM-এর API-ই সেরা। এখানে চারটি table আর যোগফল আছে, তাই পরের প্রশ্ন।
  2. ORM কি পারে? প্রতিটি ক্যাটাগরির ভেতরে ক্রম লাগবে, যা Prisma-র সাধারণ API-তে সরাসরি নেই। সব সারি এনে JavaScript-এ হিসাব চলে, কিন্তু ২০,০০০ সারি টানা ভুল জায়গায় কাজ করা।
  3. নতুন tool? Kysely-তে window function আছে। কিন্তু একটি query-র জন্য আরেকটি dependency, আরেকটি type উৎস আনা বাড়তি খরচ।
  4. পথ আছে তো? Prisma-র $queryRaw tagged template দিয়ে parameter নিরাপদ রাখে। তাই tool না বদলেই কাজ হয়।
  5. দাম চুকানো। Raw পথে type নিজের দায়িত্ব। BigInt সামলান, ফল যাচাই করুন, EXPLAIN দেখুন, আর একটি test লিখুন।
  6. সিদ্ধান্ত। Raw SQL-টি একটি repository function-এর ভেতরে থাকবে, যাতে বাকি কোড SQL না দেখে। ভিন্ন প্রকল্পে উত্তর ভিন্নও হতে পারে।

কোন ডেটাবেস

Tool-এর আগে ডেটাবেসের প্রশ্ন। সংক্ষেপে, আর মনে রাখবেন এগুলো সাধারণ প্রবণতা, নিয়ম নয়:

PostgreSQL

নতুন অ্যাপে অনেকের প্রথম পছন্দ। সমৃদ্ধ type, jsonb, শক্ত SQL, অনেক extension. এই কোর্সের মূল উদাহরণ। তবে সস্তা hosting-এ সবসময় মেলে না।

MySQL / MariaDB

পুরোনো, পরিচিত, বিপুলভাবে ব্যবহৃত। Shared hosting-এ প্রায়ই তৈরি থাকে। Laravel-এর জগতে খুব সাধারণ। কিছু SQL বিষয়ে PostgreSQL থেকে আলাদা।

SQLite

একটি ফাইলই ডেটাবেস, আলাদা server নেই। শেখা, test আর এক server-এর ছোট অ্যাপে চমৎকার। অনেকে একসাথে লিখলে বা একাধিক server হলে সীমা আসে।

বাংলাদেশের অনেক shared hosting প্ল্যানে, সাধারণত cPanel-এর সাথে, MySQL বা MariaDB দেওয়া থাকে, আর PostgreSQL থাকতেও পারে, না-ও পারে। কেনার আগে আপনার provider-কে জিজ্ঞেস করুন। আরও তিনটি প্রশ্ন করুন: Node.js অ্যাপ দীর্ঘক্ষণ চালানো যায় কি না, প্রতি user-এর সংযোগের সীমা কত, আর ডেটাবেসে বাইরে থেকে সংযোগ করা যায় কি না। সীমা কম হলে pool-ও ছোট রাখতে হবে। Prisma-য় MySQL-এর জন্য আলাদা driver adapter লাগে (docs-এ mariadb driver), আর SQLite-এর জন্য @prisma/adapter-better-sqlite3. কোথায় চালাবেন, সেই পুরো তুলনা পর্ব ৮২-তে।

আরও গভীরে: PostgreSQL প্রতিটি সংযোগে একটি OS process চালায়, আর সেই process-এর নিজের মেমরি থাকে, তাই max_connections বাড়ানো বিনামূল্যে নয়। Transaction-mode pooler সংযোগ ভাগ করে শুধু transaction-এর সময়টুকু, তাই session-ভিত্তিক সুবিধা ভাঙতে পারে। PgBouncer-এর তালিকায় SET, LISTEN, session-level advisory lock আর SQL-এর PREPARE সেখানে চলে না। Protocol-স্তরের prepared statement-এর জন্য তার max_prepared_statements সেট করতে হয়।

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

  • ORM আর query builder-এর পার্থক্য? Builder SQL-এর গঠন বানায়। ORM object, সম্পর্ক আর লোডিং-ও সামলায়।
  • Parameterized query কেন injection ঠেকায়? SQL-এর গঠন আর মান আলাদা যায়, তাই মান আদেশ হতে পারে না।
  • Connection pool কেন? সংযোগ বানানো দামি, আর ডেটাবেসের সংযোগের সংখ্যা সীমিত।
  • Serverless-এ সমস্যা কী? প্রতিটি কপির আলাদা pool মিলে ডেটাবেসের সীমা ছাড়িয়ে যায়।
  • Active Record আর Data Mapper? প্রথমটিতে object নিজে সেভ করে, দ্বিতীয়টিতে আলাদা স্তর করে।

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

“Prisma দিয়ে products API লিখে দাও” বললে AI দ্রুত কোড দেয়। এই জায়গায় AI-এর ভুল দুই ধরনের: পুরোনো API, আর চলে এমন কিন্তু বিপজ্জনক কোড। Prisma-র version দ্রুত বদলায়, তাই পুরোনো উদাহরণের সম্ভাবনা বেশি।

AI-এর ভুলকেন ক্ষতিকরযেভাবে ধরবেন
$queryRawUnsafe-এ template stringSQL injectionDiff-এ “Unsafe” খুঁজুন, log-এ parameter দেখুন
Handler-এর ভেতরে new PrismaClient()প্রতি request-এ নতুন poolচাপ দিয়ে সংযোগ গুনুন
URL-এ connection_limit, Prisma 7-এপুরোনো পথ, সীমা কাজ না করতে পারেনিজের version-এর docs দেখুন
provider = "prisma-client-js" আর adapter ছাড়া clientপুরোনো সেটআপচালিয়ে দেখুন, upgrade guide পড়ুন
prisma@latestRelease candidate আসতে পারেnpm view prisma dist-tags
res.json()-এ raw COUNT-এর ফলBigInt-এ crashআসল ডেটায় route চালান
where: req.bodyClient filter-এর গঠন ঠিক করেZod দিয়ে নির্দিষ্ট ঘর নিন

নিচের prompt-টি version, নিরাপত্তা আর প্রমাণ, তিনটিই চায়। লক্ষ করুন, এটি তৈরি হওয়া SQL দেখতেও বলে।

আমি Node.js 24, TypeScript আর PostgreSQL দিয়ে একটি অনলাইন দোকানের API লিখছি। Prisma-র stable version 7.10.1 ব্যবহার করব, driver adapter @prisma/adapter-pg সহ। দরকার: ক্যাটাগরি slug দিয়ে পণ্যের তালিকা, ক্যাটাগরির নামসহ, আর প্রতি ক্যাটাগরির সেরা-বিক্রীত পণ্যের একটি রিপোর্ট। শর্ত: ১. PrismaClient একবারই তৈরি হবে, pool-এর max আর timeout নিজে ঠিক করবে। ২. ইনপুট Zod দিয়ে যাচাই হবে, where-এ req.query সরাসরি যাবে না। ৩. রিপোর্টে শুধু $queryRaw tagged template, কোনো Unsafe method নয়। BigInt সামলাবে। ৪. Query log চালু করার কোড দাও, আর বলো প্রতিটি কাজে কয়টি SQL যাবে বলে তুমি মনে করো। যে API নিয়ে নিশ্চিত নও, সেটি বলো, বানিয়ে লিখো না। আমি এটি চালিয়ে log মিলিয়ে দেখব।

AI-এর লেখা ডেটাবেস কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Object আর table-এর অমিল এত পুরোনো হলে সরাসরি object সংরক্ষণের ডেটাবেস কেন জিতল না? ORM-এর দরকারই বা থাকল কেন?

Edgar Codd ১৯৭০ সালে relational model-এর প্রস্তাব দেন। তাঁর মূল ধারণা ছিল ডেটাকে প্রোগ্রামের গঠন থেকে আলাদা রাখা। আশির দশকের শেষে আর নব্বইয়ের দশকে object-oriented ভাষা ছড়াল, আর অনেকে ভাবলেন উত্তর সোজা: object সরাসরি জমা রাখা হোক। Object database নামে বেশ কিছু পণ্য এলো, মানদণ্ড তৈরির চেষ্টাও হলো। প্রযুক্তিটি কাজ করেছিল, কিন্তু মূলধারা দখল করতে পারেনি।

কারণগুলোর একটি হলো ডেটার আয়ু। প্রোগ্রাম বদলায়, ভাষা বদলায়, কিন্তু একটি দোকানের বিক্রির হিসাব বিশ বছর থাকতে পারে। Object-এর গঠনে বাঁধা ডেটা সেই একটি প্রোগ্রামের চোখেই দেখা। Table আর SQL-এ একই ডেটা রিপোর্ট টুল, হিসাব বিভাগ আর আগামী দিনের নতুন অ্যাপ, সবাই নিজের মতো জিজ্ঞেস করতে পারে। আপনি object-এর পথে একটি সম্পর্ক ধরে চলেন, কিন্তু SQL-এ যেকোনো দিক থেকে প্রশ্ন করা যায়, যেটি আগে ভাবা ছিল না।

তাই অমিলটি কোনো দুর্ঘটনা নয়। দুটি জগৎ দুটি ভিন্ন কাজে ভালো: object আচরণ ধরে, table তথ্য ধরে। ORM সেই ফাঁকের সেতু। ২০০৬ সালে Ted Neward একে “কম্পিউটার বিজ্ঞানের ভিয়েতনাম” বলেছিলেন, আর ২০১২ সালে Martin Fowler “OrmHate” লেখায় যুক্তি দেন যে সমস্যাটিই কঠিন, ORM তার উপযুক্ত মূল্য চোকাচ্ছে। সাম্প্রতিক কালে Drizzle আর Kysely-র মতো tool ফাঁকটি লুকানোর বদলে দেখিয়ে রাখার পথ নিয়েছে। আজ PostgreSQL-এর jsonb আবার অনেকটা document রাখার সুবিধা এনেছে, অর্থাৎ দুই জগৎ কিছুটা কাছে এসেছে, কিন্তু মিলে যায়নি।

মূল উপলব্ধি: ডেটা প্রোগ্রামের চেয়ে বেশিদিন বাঁচে, তাই ডেটার গঠনকে একটি প্রোগ্রামের সুবিধার কাছে বেঁধে ফেলা ঠিক নয়। ORM একটি অনুবাদক, আর অনুবাদের দরকার থাকবেই, কারণ দুই ভাষার কাজই আলাদা।

প্র ০২Pool যত বড় তত ভালো নয় কেন? আমাদের run-এ তো max ২০ সবচেয়ে দ্রুত ছিল।

আমাদের run-এর query ছিল pg_sleep. ঘুমন্ত query CPU, disk বা lock চায় না, তাই একসাথে যত খুশি ঘুমানো যায়। আসল query তা নয়। একটি ৪ core-এর ডেটাবেসে একই সময়ে ৪টি query-ই আসলে CPU-তে চলতে পারে। ১০০টি সক্রিয় সংযোগ থাকলে বাকিরা পালা করে চলে, আর প্রতিবার পালা বদলাতে context switch আর cache নষ্ট হওয়ার খরচ আছে। তার সাথে lock নিয়ে প্রতিযোগিতা আর মেমরির চাপ বাড়ে, কারণ প্রতিটি সংযোগ query-র ভেতরে work_mem পর্যন্ত নিতে পারে।

রাস্তার কথা ভাবুন। ঢাকার কোনো মোড়ে একসাথে সব রিকশা ঢুকলে কেউই এগোতে পারে না। ট্রাফিক পুলিশ কয়েকটি করে ছাড়লে মোট বেশি গাড়ি পার হয়। PostgreSQL wiki-র “Number Of Database Connections” পাতা ঠিক এই কথাই বলে: সম্পদ পূর্ণ হওয়ার পরে বাড়তি সংযোগ কাজ বাড়ায় না, বরং throughput কমাতে পারে। সেখানে একটি শুরুর সূত্রও আছে: সক্রিয় সংযোগ প্রায় (core_count * 2) + effective_spindle_count. ডেটা পুরো cache-এ থাকলে spindle সংখ্যা শূন্য ধরা হয়। অর্থাৎ ৮ core-এ শুরুর সংখ্যা প্রায় ১৬. এটি একটি শুরুর বিন্দু, চূড়ান্ত উত্তর নয়।

আরেকটি হিসাব হলো Little’s Law: গড়ে একই সময়ে চলা কাজ = আসার হার × প্রতিটির সময়। সেকেন্ডে ৪০০টি query, প্রতিটি ১৫ ms হলে গড়ে ৪০০ × ০.০১৫ = ৬টি সংযোগ ব্যস্ত। তাই পুরো অ্যাপের জন্য ১০ বা ২০-র pool প্রায়ই যথেষ্ট, যা অনেকে আন্দাজ করেন তার চেয়ে কম। ভিড়ের সময় সারি একটি ভালো জিনিস: সে চাপকে অ্যাপে রাখে, ডেটাবেসে ঢুকতে দেয় না।

মূল উপলব্ধি: Pool ডেটাবেসকে রক্ষা করার একটি ফটক। ছোট ফটকে কিছু অপেক্ষা হয়, কিন্তু ভেতরে কাজ চলতে থাকে। বড় ফটকে সবাই ঢুকে সবার গতি নষ্ট করে। সংখ্যা আন্দাজে নয়, মেপে ঠিক করুন।

প্র ০৩Serverless-এ সংযোগের সমস্যা কি PostgreSQL-এর ত্রুটি? PgBouncer লাগালেই কি সব মিটে যায়?

এটি দুই পক্ষের নকশার সংঘাত। প্রচলিত ডেটাবেসের ধারণা ছিল অল্প কয়েকটি দীর্ঘজীবী application server, যারা কিছু সংযোগ খুলে রাখবে। Serverless-এর ধারণা উল্টো: অসংখ্য স্বল্পজীবী কপি, যাদের সংখ্যা ভিড় ঠিক করে, আপনি নন। দুই নকশা নিজের জায়গায় যুক্তিসঙ্গত। সমস্যা হয় একসাথে বসালে। আমাদের run-এ শুধু গুণ করলেই পাওয়া গেছে: ২৫ × ৫ = ১২৫, সীমা ১০০.

PgBouncer-এর মতো pooler ২০০০-এর দশক থেকেই আছে, আর সে চমৎকার সমাধান। কিন্তু বিনামূল্যে নয়। তার transaction mode-এ একটি client-এর পরের transaction অন্য আসল সংযোগে চলতে পারে। তাই সংযোগে বাঁধা যেকোনো অবস্থা হারাতে পারে। PgBouncer-এর নিজের তালিকা বলে এই mode-এ SET, LISTEN, WITH HOLD cursor আর session-level advisory lock চলে না। ধরুন আপনার কোড SET app.user_id দিয়ে row-level নিরাপত্তা চালায়, তাহলে ভুল সংযোগে ভুল মান থাকলে বিপজ্জনক ফল হতে পারে। এছাড়া ORM-এর migration সাধারণত সরাসরি সংযোগ চায়, তাই দুটি URL রাখতে হতে পারে।

আরও একটি স্তর মানে আরও একটি জিনিস চালানো, আর ব্যর্থ হওয়ার আরেকটি জায়গা। অনেক managed ডেটাবেস provider এখন pooler সাথেই দেয়। কেউ কেউ HTTP বা WebSocket-ভিত্তিক driver দেয়। আবার এটিও ভাবার মতো: সত্যিই কি serverless দরকার? ২৪ ঘণ্টা চলা একটি ছোট Node server আর একটি pool অনেক ছোট অ্যাপে অনেক সরল। পর্ব ৮২-তে এই তুলনা আসবে।

Pooler অ্যাপের নিজের দোষও ঢাকে না। Query ধীর হলে সংযোগ বেশিক্ষণ ধরা থাকে, আর সবচেয়ে ভালো pooler-ও সারি জমতে দেখবে। লম্বা transaction-এর ভেতরে বাইরের API call করলে সংযোগ অকারণে আটকে থাকে।

মূল উপলব্ধি: সংযোগ একটি সীমিত সম্পদ। Serverless সেই সীমাকে চোখের আড়াল করে, মিটিয়ে দেয় না। Pooler একটি ভালো হাতিয়ার, তবে এর নিজের নিয়ম মেনে কোড লিখতে হয়।

প্র ০৪ORM ব্যবহার করলে কি SQL injection নিয়ে আর ভাবতে হয় না? ORM কি সব আক্রমণ বন্ধ করে দেয়?

না। ORM সাধারণ পথটি নিরাপদ করে, যা বিশাল উপকার। SQL injection বহু দশকের পুরোনো সমস্যা, আর ২০০৭ সালের xkcd-র “Bobby Tables” কৌতুক এটিকে সাধারণ মানুষের কাছেও পরিচিত করেছে। OWASP-এর ঝুঁকির তালিকাতেও injection দীর্ঘদিন ধরে আছে। ORM-এ মান নিজে থেকেই parameter-এ যায়, তাই সবচেয়ে সাধারণ ভুলটি কঠিন হয়ে যায়। কিন্তু সুরক্ষার সীমা আছে।

প্রথম ফাঁক হলো raw পথ। আমাদের run-এ $queryRawUnsafe-এ string জোড়া দেওয়ায় ০-এর জায়গায় ৮ এলো। AI বা তাড়াহুড়ো করা ডেভেলপার ঠিক এখানেই ভুল করেন, বিশেষ করে যখন কলামের নাম পরিবর্তনশীল করতে হয়, আর tagged template সেটি পারে না। দ্বিতীয় ফাঁক হলো নাম। ORDER BY, কলাম আর table-এর নাম parameter হতে পারে না। সেখানে allowlist লাগে।

তৃতীয় ফাঁক আরও সূক্ষ্ম, আর ORM-এরই নিজের। prisma.product.findMany({ where: req.body }) লিখলে SQL injection হয় না, সব মান parameter-এ যায়। কিন্তু client এখন filter-এর পুরো গঠন লিখতে পারে: লুকানো কলামে filter, যেমন ক্রয়মূল্য। JSON-এ { "costPrice": { "lt": 500 } } পাঠিয়ে ভাগ করে করে গোপন মান বের করা যায়, যা পর্ব ৩৪-এ দেখেছেন। একইভাবে data: req.body দিলে mass assignment হয়: কেউ role বা price পাঠিয়ে দিতে পারে। এই ঝুঁকিগুলো SQL-এর স্তরে নয়, ডেটার গঠনের স্তরে। আর অনুমতির প্রশ্ন, কে কোন সারি দেখবে, ORM-এর আওতার বাইরে। সেটি পর্ব ৪৮-এর বিষয়।

মূল উপলব্ধি: ORM ভুল করা কঠিন করে, অসম্ভব করে না। ইনপুটকে সবসময় অবিশ্বস্ত ধরুন: মান parameter-এ, নাম allowlist-এ, আর object-এর গঠন Zod দিয়ে বাঁধা।

প্র ০৫“ORM থাকলে পরে সহজে ডেটাবেস বদলানো যাবে” — এই প্রতিশ্রুতি কতটা সত্যি?

ডেটাবেস-নিরপেক্ষতা ORM-এর পুরোনো বিক্রয় যুক্তি। সহজ কাজে এটি প্রায় সত্যি। findMany, create, update একই থাকতে পারে, আর tool নিজে $1 আর ?-এর পার্থক্য সামলায়। ছোট অ্যাপে SQLite থেকে PostgreSQL-এ যাওয়া তাই সম্ভব কাজ। কিন্তু প্রতিশ্রুতিটি ভাঙতে শুরু করে ঠিক যেখানে প্রকল্প গুরুত্বপূর্ণ হয়ে ওঠে।

প্রথমত, আচরণ। ধারণা করুন একটি ডেটাবেস ডিফল্টে text-কে case-insensitive তুলনা করে, আরেকটি করে না। একই where: { name: 'dal' } ভিন্ন ফল দিতে পারে, আর কোনো error আসবে না। তারিখ, দশমিক, ক্রম আর locale, সবখানে এমন পার্থক্য থাকে। Prisma-র docs-ও বলে, raw query-র ফলের ধরন ডেটাবেসভেদে এক নয়, যেমন MySQL-এ boolean আসে ১ বা ০, PostgreSQL-এ true বা false.

দ্বিতীয়ত, শক্তি। আপনি যে কারণে একটি ডেটাবেস বেছেছেন, যেমন PostgreSQL-এর jsonb বা বিশেষ index, সেটি ব্যবহার করলেই নিরপেক্ষতা শেষ। আর ব্যবহার না করলে দামি ডেটাবেসটি সস্তা সাধারণ হাতিয়ার হয়ে রইল। সব raw SQL, trigger আর migration ইতিহাস আবার লিখতে হয়, ডেটা সরাতে হয়, আর সবচেয়ে বড় কথা, চালু অ্যাপে কোনো সময় নষ্ট না করে সেটি করতে হয়।

তৃতীয়ত, অভিজ্ঞতার কথা। বেশিরভাগ প্রকল্প জীবনে কখনো ডেটাবেস বদলায় না। অকারণে নিরপেক্ষতা ধরে রাখতে সব কিছুকে সবচেয়ে সাধারণ স্তরে রাখা প্রায়ই খরচ বেশি, লাভ কম। একই কথা test-এও খাটে: production-এ PostgreSQL হলে test SQLite-এ চালানো আকর্ষণীয়, কিন্তু পার্থক্যগুলো ঠিক সেখানেই লুকায়। পর্ব ৭৫-এ test ডেটাবেস নিয়ে কথা হবে।

মূল উপলব্ধি: ORM অনুবাদের সুবিধা দেয়, ডেটাবেসের চরিত্র মুছে দেয় না। একটি ডেটাবেস সচেতনভাবে বেছে নিন, তার শক্তি ব্যবহার করুন, আর বদল যদি দরকার হয়, সেটিকে একটি সত্যিকারের প্রকল্প হিসেবে ধরুন।

অনুশীলন

প্রথমে নিজে করে দেখুন, তারপর উত্তর মেলান।

  1. হিসাব: ডেটাবেসের max_connections ১০০, এর মধ্যে ৩টি superuser-এর জন্য সংরক্ষিত, আর অ্যাপ সাধারণ user. (ক) সেকেন্ডে ২০০ request, প্রতিটিতে ২টি query, প্রতিটি query ১৫ ms. গড়ে কয়টি সংযোগ ব্যস্ত? (খ) Serverless-এ ১২টি কপি, প্রতিটিতে pool ১০. সব সংযোগ ভরলে কী হবে? (গ) সীমা না ছাড়িয়ে প্রতিটি কপির সর্বোচ্চ pool কত? (ঘ) max ৪-এর pool-এ একসাথে ৩০টি query, প্রতিটি ৫০ ms. সূত্রে মোট সময় কত?

    (ক) সেকেন্ডে ২০০ × ২ = ৪০০ query. L = ৪০০ × ০.০১৫ = ৬টি সংযোগ, গড়ে।

    (খ) ১২ × ১০ = ১২০ চাওয়া, পাওয়া যাবে ১০০ − ৩ = ৯৭টি। ২৩টি সংযোগ ব্যর্থ হবে, too many clients ধরনের error সহ।

    (গ) ⌊৯৭ / ১২⌋ = ৮, মোট ৯৬টি। তবু migration বা অন্য tool-এর জন্য জায়গা রাখা ভালো, তাই আরও কম রাখা উচিত।

    (ঘ) ⌈৩০ / ৪⌉ × ৫০ = ৮ × ৫০ = ৪০০ ms, সংযোগ খোলার সময় বাদে।

  2. Output অনুমান: new Pool({ max: 2, connectionTimeoutMillis: 300 }) বানানো হলো। কোড পরপর তিনবার await pool.connect() ডাকে, কেউ release() করে না। (ক) দ্বিতীয় connect-এর পরে totalCount আর idleCount কত? (খ) তৃতীয় connect-এ কী হবে? (গ) PostgreSQL কি বন্ধ বা ধীর?

    (ক) totalCount ২, idleCount ০. একটি বাস্তব run-এ এটিই এসেছে।

    (খ) ৩০০ ms অপেক্ষার পরে error: timeout exceeded when trying to connect.

    (গ) না। ডেটাবেস ফাঁকা বসে আছে। সমস্যা অ্যাপের ভেতরে, দুটি সংযোগ ফেরত দেওয়া হয়নি। pool.query() ব্যবহার করুন, অথবা finally-তে release().

  3. Code review: AI নিচের route দিয়েছে। কমপক্ষে পাঁচটি সমস্যা ধরুন, আর প্রতিটির সমাধান বলুন।
    app.get('/api/report', async (req, res) => {
      const prisma = new PrismaClient({
        adapter: new PrismaPg({ connectionString: 'postgresql://admin:example-secret@db/shop' }),
      });
      const { sort, ...filters } = req.query;
      const products = await prisma.product.findMany({ where: filters as any });
      const total = await prisma.$queryRawUnsafe(
        `SELECT COUNT(*) AS n FROM products ORDER BY ${sort}`);
      res.json({ products, total });
    });
    • Handler-এ new PrismaClient(): প্রতি request-এ নতুন pool, শিগগিরই সংযোগ ফুরাবে। সমাধান: একটি module-এ একবার।
    • কোডে connection string: গোপন মান repository-তে যায়। সমাধান: environment variable.
    • where: filters as any: Client filter-এর গঠন ঠিক করে, আর as any type-ও বন্ধ করে দিল। সমাধান: Zod, নির্দিষ্ট ঘর।
    • $queryRawUnsafe + ${sort}: SQL injection. সমাধান: allowlist থেকে নিজের SQL টুকরো, মান tagged template-এ।
    • COUNT-এর BigInt সরাসরি res.json-এ: Do not know how to serialize a BigInt. সমাধান: ::int বা Number().
    • select আর take নেই: সব কলাম আর সব সারি, তাই ক্রয়মূল্যের মতো ঘরও বেরোতে পারে। সমাধান: নির্দিষ্ট select আর সীমা।
    • Error handling আর auth নেই: রিপোর্ট কে দেখবে? পর্ব ২৮-এর central handler ব্যবহার করুন, আর পর্ব ৪৮-এর permission যোগ করুন।
  4. Command আর নকশা: একটি প্রকল্পে আপনি prisma.order.findMany({ include: { customer: true } }) লিখেছেন। কত query যায়, সেটি আন্দাজ না করে কীভাবে জানবেন? ধাপগুলো লিখুন। তারপর কোন version ব্যবহার করছেন, আর latest কী, সেটি কোন command দেখায়?

    PrismaClient-এ log: [{ emit: 'event', level: 'query' }] দিন, $on('query', …)-এ e.query আর e.params ছাপুন, route একবার চালান, আর log-এর লাইন গুনুন। Prisma 7.10.1-এ সাধারণ include আলাদা query পাঠাতে পারে, তবে আপনার version-এ যা দেখবেন সেটিই সত্য।

    npx prisma --version বর্তমান version দেখায়, আর npm view prisma dist-tags সব tag. লেখার সময়ে এর latest ছিল 8.0.0-rc.29, যা একটি release candidate, তাই install-এ prisma@7.10.1 লেখা হয়েছে।

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

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

ক্রমএকটি ORM call কীভাবে ফল হয়, ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. কোডে query object লিখে await করা
  2. ORM সেই বর্ণনা থেকে SQL আর parameter তৈরি করে
  3. Pool থেকে একটি সংযোগ ধার নেওয়া হয়
  4. ডেটাবেস query চালিয়ে সারি ফেরত দেয়
  5. ORM সারি জুড়ে JavaScript object বানিয়ে দেয়

Query builder আর ORM-এর মূল পার্থক্য কোনটি?

  • Query builder SQL পাঠায়, ORM পাঠায় না
  • Builder SQL-এর গঠন বানায়, ORM সম্পর্ক আর object-এর দিকটিও সামলায়
  • শুধু ORM parameter ব্যবহার করে
  • Query builder শুধু JavaScript-এ চলে, TypeScript-এ নয়

একটি products table-এ "SELECT * FROM products WHERE slug = '" + input + "'" লেখা আছে, আর input হলো x' OR '1'='1. একই input parameter $1 দিয়ে পাঠালে পার্থক্য কী হবে?

  • দুই ক্ষেত্রেই সব সারি আসবে
  • জোড়া দিলে ০টি সারি, parameter-এ সব সারি
  • জোড়া দিলে সব সারি আসতে পারে, parameter-এ মিল না থাকলে ০টি
  • Parameter দিলে syntax error হবে

max ১০-এর একটি connection pool-এ একসাথে ৩০টি query এলো, প্রতিটি ১০০ ms. সংযোগ খোলার সময় বাদে মোট কত সময়ের কাছাকাছি লাগবে?

  • ১০০ ms
  • ২০০ ms
  • ৩,০০০ ms
  • ৩০০ ms

৩০টি serverless কপি, প্রতিটির pool ৫, আর ডেটাবেসের সীমা ১০০. সবচেয়ে সরাসরি ঝুঁকি কী?

  • মোট চাওয়া ১৫০ পর্যন্ত হতে পারে, তাই কিছু সংযোগ ব্যর্থ হবে
  • সব কপি একটিই pool ভাগ করায় query ধীর হবে
  • TypeScript-এর type মিলবে না
  • Parameter কাজ করা বন্ধ করবে, তাই injection হবে

product.save() লিখে একটি object নিজেকে ডেটাবেসে রাখে। এটি কোন নকশা?

  • Data Mapper
  • Active Record
  • Query builder
  • Connection pool

Prisma-য় $queryRaw tagged template আর $queryRawUnsafe-তে string জোড়া দেওয়ার পার্থক্য কী?

  • কোনো পার্থক্য নেই, দুটিই একই SQL পাঠায়
  • Tagged template-এ window function চলে না
  • Tagged template-এ বসানো মান parameter হয়, জোড়া দেওয়া string SQL-এর অংশ হয়
  • Tagged template ফলের type ডেটাবেসের সাথে নিজে থেকে যাচাই করে

শব্দকোষ

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

মনে রাখুন

  1. Raw SQL, query builder আর ORM, সবাই শেষে SQL পাঠায়। পার্থক্য হলো কতটা আপনি লেখেন আর কতটা দেখতে পান।
  2. ORM কী পাঠাল, তা log ছাড়া জানা যায় না। একটি call দুটি query-ও হতে পারে।
  3. মান সবসময় parameter-এ, নাম allowlist থেকে। $queryRawUnsafe-এ string জোড়া নয়।
  4. Pool-এর সময় ⌈N / max⌉ × s. Serverless-এ কপির সংখ্যা × pool ডেটাবেসের সীমা ছাড়াতে পারে।
  5. Type safety compile-এর সময়ের। Overhead ছোট, কিন্তু লুপে query বড় ক্ষতি করে। ডেটাবেস আর tool আপনার প্রকল্প দেখে বাছুন।

পরের পর্বে: এবার আমরা বাজারঘরের জন্য একটি পথ বেছে নিচ্ছি। পর্ব ৩৯-এ (Prisma — schema, client আর CRUD) schema লিখে, client তৈরি করে, সত্যিকারের ডেটা পড়া আর লেখা শুরু হবে।

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

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