সরাসরি পর্বে যান
পর্ব ৪৯ · ৫৯-এর মধ্যে · মডিউল ৭

JDBC আর ডেটাবেস — SQL, PreparedStatement আর transaction

JDBC and Databases — Relational Basics, Essential SQL, PreparedStatement, SQL Injection, Transactions, Connection Pools and the Repository Pattern

Taught in Bangla, this lesson connects Java to relational databases: tables, keys and essential SQL with JOIN and GROUP BY, the JDBC path from driver and Connection to PreparedStatement and ResultSet, preventing SQL injection, generated keys and batches, transactions and ACID, connection pools, the Repository pattern, the N+1 problem, migrations and safe credentials.

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

  • ফাইল কোথায় যথেষ্ট আর কোথায় ডেটাবেস লাগে; টেবিল, সারি, কলাম, primary key আর foreign key
  • দরকারি SQL — CREATE TABLE, INSERT, SELECT (WHERE, ORDER BY, JOIN, GROUP BY), UPDATE, DELETE
  • JDBC-র পুরো পথ: driver, connection URL, PreparedStatement, ResultSet থেকে record, আর উল্টো ক্রমে বন্ধ
  • SQL injection কীভাবে ঘটে আর ? placeholder কেন তা ঠেকায়; generated key, batch, SQLException আর SQLState
  • transaction আর ACID, connection pool, Repository pattern, N+1 সমস্যা, টেস্টিং, migration আর নিরাপদ credential

১ · ফাইল থেকে ডেটাবেস — কেন দরকার

পর্ব ৩৭-এ ফাইলে লেখা শিখেছেন, পর্ব ৩৯-এ JSON. ছোট একটি টুলের সেটিং বা একজনের খরচের হিসাব রাখতে এগুলোই যথেষ্ট। কিন্তু একটি অনলাইন দোকান ভাবুন — শত শত ক্রেতা একসাথে অর্ডার দিচ্ছেন, স্টক কমছে, পেমেন্ট আসছে। এখানে সাধারণ ফাইল তিন জায়গায় হার মানে:

একসাথে অনেকে

দুজন একই মুহূর্তে শেষ একটি শাড়ি কিনলে কে পাবেন? ফাইলে দুজনেই পুরোনো স্টক পড়ে দুজনেই লিখে দেবেন — পর্ব ৪০-এর lost update, এবার ডিস্কে।

concurrency

দ্রুত খোঁজা

দশ লাখ ক্রেতার মধ্যে একটি ফোন নম্বর খুঁজতে পুরো ফাইল পড়তে হয়। ডেটাবেস index রেখে প্রায় সাথে সাথে খুঁজে দেয়।

query

মাঝপথে বিপদ

এক অ্যাকাউন্ট থেকে টাকা কাটা হলো, অন্যটিতে যোগ করার আগেই লোডশেডিং। ফাইলে টাকা উধাও। ডেটাবেস নিশ্চিত করে — দুটিই হবে, নয়তো একটিও না।

transaction

এই তিনটি সমস্যা সমাধানের জন্যই ডেটাবেস. ডেটাবেস আলাদা একটি প্রোগ্রাম (server), যা ডেটা ডিস্কে রাখে, অনেক client-এর অনুরোধ একসাথে সামলায়, আর crash-এর পরও ডেটা ঠিক রাখে। প্রোগ্রাম বন্ধ হলেও ডেটা টিকে থাকে — একে বলে persistence. আপনার Java প্রোগ্রাম সেই server-এর সাথে কথা বলে একটি ভাষায়, যার নাম SQL.

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

ডেটাবেস অনেক ধরনের, তবে ব্যবসার বেশিরভাগ ডেটা — অর্ডার, লেনদেন, ক্রেতা — রাখা হয় relational database-এ: PostgreSQL, MySQL, Oracle, SQL Server, আর ছোট কাজে SQLite. এগুলোর সাথে Java কথা বলে JDBC (Java Database Connectivity) দিয়ে — JDK-র java.sql package-এর কয়েকটি interface, যা প্রতিটি ডেটাবেসের জন্য একই। প্রতিটি ডেটাবেস কোম্পানি একটি JDBC driver দেয়, যা সেই interface-গুলো implement করে আর নিজের ডেটাবেসের নেটওয়ার্ক-ভাষায় অনুবাদ করে। অর্থাৎ পর্ব ২১-এর “interface দিয়ে চুক্তি, implementation বদলানো যায়” নীতির সবচেয়ে বড় বাস্তব উদাহরণ এটি।

১৯৭০ সালে IBM-এর গবেষক Edgar F. Codd “A Relational Model of Data for Large Shared Data Banks” প্রবন্ধে প্রস্তাব করেন: ডেটা থাকবে গাণিতিক relation-এ, অর্থাৎ টেবিলে, আর প্রোগ্রামকে জানতে হবে না ডেটা ডিস্কে কীভাবে সাজানো। IBM-এর System R প্রজেক্টে Donald Chamberlin আর Raymond Boyce এর জন্য SEQUEL ভাষা বানান, যা পরে নাম বদলে SQL হয়। Java-র JDBC আসে ১৯৯৭ সালে JDK 1.1-এর সাথে, Microsoft-এর ODBC-র নকশা অনুসরণ করে — বিস্তারিত ভাবনার প্রশ্নে।

২ · relational মডেল — টেবিল, সারি, কলাম আর key

relational ডেটাবেসে প্রতিটি ধরনের জিনিসের জন্য একটি টেবিল: ক্রেতাদের একটি, অর্ডারের একটি। টেবিলের প্রতিটি কলাম একটি বৈশিষ্ট্য (নাম, ফোন), আর প্রতিটি সারি (row) একটি জিনিস — একজন ক্রেতা। Java-র ভাষায়: টেবিল ≈ class, কলাম ≈ field, সারি ≈ object — কিন্তু মিলটা পুরোপুরি নয়, সেই গল্প ভাবনার প্রশ্নে।

ছোট একটি দোকানের দুটি টেবিল

customers and orders — linked by a foreign key

customers                                    orders
┌────┬────────┬─────────────┬──────────┐     ┌─────┬─────────────┬───────┐
│ id │ name   │ phone       │ district │     │ id  │ customer_id │ total │
├────┼────────┼─────────────┼──────────┤     ├─────┼─────────────┼───────┤
│ 1  │ রহিম   │ 01711000001 │ ঢাকা     │ ◄── │ 101 │ 1           │ 1200  │
│ 2  │ করিম   │ 01811000002 │ রাজশাহী  │ ◄┐  │ 102 │ 1           │  800  │
│ 3  │ সুমি   │ 01911000003 │ ঢাকা     │  └─ │ 103 │ 2           │ 1500  │
└────┴────────┴─────────────┴──────────┘     │ 104 │ 3           │  500  │
  ▲ primary key                              └─────┴─────────────┴───────┘
                                                     ▲ foreign key → customers.id
ছবি ৪৭.১ — প্রতিটি অর্ডারে ক্রেতার পুরো তথ্য নয়, শুধু তাঁর id. ঠিকানা বদলালে এক জায়গায় বদলালেই চলে।
  • Primary key: যে কলাম প্রতিটি সারিকে আলাদা করে চেনায় — এখানে id. দুটি সারির একই primary key থাকতে পারে না, আর এটি কখনো খালি (NULL) নয়। ফোন নম্বরও আলাদা, কিন্তু মানুষ নম্বর বদলান; তাই সাধারণত অর্থহীন একটি সংখ্যা primary key হয়।
  • Foreign key: অন্য টেবিলের primary key-র দিকে নির্দেশ — orders.customer_id → customers.id. ডেটাবেস নিশ্চিত করে যে ৯৯ নম্বর ক্রেতা না থাকলে তার নামে অর্ডার ঢোকানো যাবে না।
  • Schema: কোন টেবিল, কোন কলাম, কোন টাইপ, কোন নিয়ম — ডেটার কাঠামোর পুরো নকশা। JSON ফাইলে যেকোনো কিছু লেখা যায়; ডেটাবেস schema-র বাইরে কিছু নেয় না।

ক্রেতার নাম প্রতিটি অর্ডারে না লিখে আলাদা টেবিলে একবার রাখার এই রীতিকে বলে normalization. লাভ: তথ্য এক জায়গায়, তাই পরস্পরবিরোধী কপি তৈরি হয় না। দাম: অর্ডারের সাথে নাম দেখাতে দুটি টেবিল জুড়তে হয় — পরের অংশের JOIN.

৩ · দরকারি SQL — তৈরি, ঢোকানো, খোঁজা, জোড়া, গোনা

SQL একটি declarative ভাষা: আপনি বলেন কী চান, ডেটাবেস নিজে ঠিক করে কীভাবে আনবে — অনেকটা পর্ব ৩৫-এর Stream-এর মতো। প্রথমে টেবিল তৈরি (নিচের SQL-গুলো অনলাইন runner-এ চলে না; PostgreSQL বা H2-তে চলবে):

CREATE TABLE customers (
    id        BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name      VARCHAR(100) NOT NULL,
    phone     VARCHAR(15)  NOT NULL UNIQUE,
    district  VARCHAR(50)  NOT NULL
);

CREATE TABLE orders (
    id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id  BIGINT NOT NULL REFERENCES customers(id),
    total        BIGINT NOT NULL CHECK (total >= 0)    -- টাকা, পূর্ণসংখ্যায়
);

GENERATED ALWAYS AS IDENTITY মানে id ডেটাবেস নিজে দেবে (MySQL-এ একই কাজ AUTO_INCREMENT). NOT NULL, UNIQUE, CHECK, REFERENCES — এগুলো নিয়ম, যা Java কোডে ভুল থাকলেও ডেটাবেস নিজে পাহারা দেয়। টাকা কখনো FLOAT/DOUBLE-এ রাখবেন না; পূর্ণসংখ্যা বা DECIMAL (Java-তে long বা BigDecimal, পর্ব ০৬)।

-- ঢোকানো (Create)
INSERT INTO customers (name, phone, district) VALUES ('রহিম', '01711000001', 'ঢাকা');

-- খোঁজা (Read): কোন সারি (WHERE), কোন কলাম, কোন ক্রমে (ORDER BY)
SELECT name, phone FROM customers WHERE district = 'ঢাকা' ORDER BY name;

-- জোড়া: প্রতিটি বড় অর্ডারের পাশে ক্রেতার নাম
SELECT o.id, c.name, o.total
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.total >= 1000;

-- বদলানো (Update) আর মোছা (Delete) — WHERE ভুলবেন না!
UPDATE customers SET district = 'খুলনা' WHERE id = 3;
DELETE FROM orders WHERE id = 104;

এই চারটি কাজকে একসাথে বলে CRUD — Create, Read, Update, Delete; প্রায় প্রতিটি ব্যবসায়িক অ্যাপের মূল কাঠামো। JOIN দুটি টেবিলের সারি মেলায় শর্ত অনুযায়ী (c.id = o.customer_id); ওপরের উদাহরণে ফল দুটি সারি: 101 | রহিম | 1200 আর 103 | করিম | 1500.

WHERE ছাড়া UPDATE customers SET district = 'খুলনা' লিখলে সব ক্রেতার জেলা খুলনা হয়ে যাবে, আর DELETE FROM orders সব অর্ডার মুছে দেবে — কোনো সতর্কবার্তা ছাড়া। আসল ডেটাবেসে UPDATE/DELETE চালানোর আগে একই WHERE দিয়ে একটি SELECT চালিয়ে দেখে নিন কোন সারিগুলো বদলাবে।

রিপোর্ট — জেলাভিত্তিক বিক্রি

ব্যবস্থাপক জানতে চান কোন জেলা থেকে কত অর্ডার আর কত টাকার বিক্রি। এখানে লাগে aggregate function (COUNT, SUM, AVG, MAX) আর GROUP BY, যা একই জেলার সারিগুলোকে এক দলে এনে প্রতিটি দলের জন্য একটি ফল দেয়:

SELECT c.district, COUNT(o.id) AS orders, SUM(o.total) AS total
FROM orders o
JOIN customers c ON c.id = o.customer_id
GROUP BY c.district
ORDER BY total DESC;
 district | orders | total
----------+--------+-------
 ঢাকা     |      3 |  2500
 রাজশাহী  |      1 |  1500

ঢাকার তিনটি অর্ডার: রহিমের দুটি (১২০০ + ৮০০) আর সুমির একটি (৫০০)। একই হিসাব Java-তে করতে হলে সব অর্ডার মেমরিতে এনে Stream-এ groupingBy লাগত; ডেটাবেসে করলে নেটওয়ার্কে আসে মাত্র দুটি সারি। নিয়ম: ছাঁকা, জোড়া আর গোনার কাজ ডেটাবেসেই করান, Java-তে আনুন শুধু যা দেখাতে হবে।

৪ · JDBC-র পথ — driver, Connection, PreparedStatement, ResultSet

JDBC-তে চারটি মূল interface, প্রতিটি আগেরটি থেকে তৈরি হয়:

interfaceকাজদোকানের উপমায়
DataSource / DriverManagerconnection দেওয়ার কারখানাহিসাবরক্ষকের অফিস
Connectionডেটাবেসের সাথে একটি খোলা সংযোগ (একটি সেশন)হিসাবরক্ষকের সাথে ফোনলাইন
PreparedStatementএকটি SQL, যার মানগুলো ? দিয়ে পরে বসানো হয়ছাপানো ফর্ম, ফাঁকা ঘরসহ
ResultSetSELECT-এর ফল, একটি cursor দিয়ে সারি সারি পড়াউত্তরের তালিকা, এক লাইন করে

driver আর connection URL

কোন ডেটাবেসে যাবেন, তা বলে JDBC URL: jdbc:postgresql://localhost:5432/shop (PostgreSQL), jdbc:mysql://localhost:3306/shop (MySQL), jdbc:sqlite:shop.db (SQLite), আর টেস্টের জন্য মেমরিতে চলা jdbc:h2:mem:shop (H2). URL-এর দ্বিতীয় অংশ দেখে DriverManager classpath-এ থাকা driver-গুলোর মধ্যে মানানসইটি বেছে নেয়। driver আসে Maven dependency হিসেবে (পর্ব ৪৬):

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.4</version>
    <scope>runtime</scope>
</dependency>

runtime scope কেন? আপনার কোড শুধু java.sql-এর interface চেনে; driver-এর কোনো class নাম লেখে না। compile-এর সময় driver লাগে না, চলার সময় লাগে। driver না থাকলে কী হয়, তা অনলাইন runner-এই যাচাই করা যায় — সেখানে JDK-র java.sql আছে, কিন্তু কোনো driver নেই:

java.sql.SQLException: No suitable driver found for jdbc:h2:mem:shop

এর getSQLState() ফেরত দেয় 08001 — “connection স্থাপন করা যায়নি” শ্রেণির কোড। বাস্তবে এই বার্তা দেখলে প্রায় সবসময় কারণ দুটির একটি: driver dependency নেই, অথবা URL-এ বানান ভুল (jdbc:postgres://, যেখানে হওয়া উচিত jdbc:postgresql://)। পুরোনো টিউটোরিয়ালে দেখবেন Class.forName("org.postgresql.Driver") — JDBC 4.0 (Java 6) থেকে driver নিজেই নিবন্ধিত হয়, এই লাইন আর লাগে না।

একটি query-র পুরো যাত্রা

ফোন নম্বর দিয়ে একজন ক্রেতা খোঁজা — JDBC-র প্রতিটি ধাপ ধরে:

  1. connectiongetConnection()সংযোগ
  2. prepareprepareStatementফর্ম
  3. bindsetString(1, …)ঘর পূরণ
  4. executeexecuteQuery()পাঠানো
  5. iteraters.next()সারি পড়া
  6. mapnew Customer(…)record
  7. closers → ps → conউল্টো ক্রমে
ধাপ ১Connection con = ds.getConnection()
Java ──TCP──► PostgreSQL :5432
ব্যবহারকারী আর পাসওয়ার্ড যাচাই, সেশন তৈরি
নতুন সংযোগে কয়েক মিলিসেকেন্ড বা তারও বেশি লাগে
ধাপ ২con.prepareStatement(sql)
SELECT id, name, phone, district
FROM customers WHERE phone = ?
SQL-এর কাঠামো স্থির — ? শুধু একটি মানের জায়গা
ধাপ ৩ps.setString(1, phone)
parameter 1 = "01811000002"
index শুরু 1 থেকে, 0 নয়
মান SQL-এর লেখায় জোড়া হয় না, আলাদা যায়
ধাপ ৪ResultSet rs = ps.executeQuery()
SQL + parameter ──► ডেটাবেস
ডেটাবেস plan বানায়, phone-এর index দিয়ে খোঁজে
ফল ফেরত আসে, cursor থাকে প্রথম সারির আগে
ধাপ ৫while (rs.next())
rs.next() → true   cursor সারি ১-এ
  id=2, name=করিম, phone=01811000002, district=রাজশাহী
rs.next() → false  আর সারি নেই
ধাপ ৬সারি থেকে record
new Customer(rs.getLong("id"), rs.getString("name"),
             rs.getString("phone"), rs.getString("district"))
→ Customer[id=2, name=করিম, ...]
ধাপ ৭try-with-resources শেষ
rs.close()   ← শেষে খোলা, আগে বন্ধ
ps.close()
con.close()  ← pool থাকলে আসলে ফেরত দেওয়া
  1. সংযোগ নেওয়া। Connection মানে ডেটাবেস server-এর সাথে একটি নেটওয়ার্ক সেশন — খোলা ব্যয়বহুল, আর server একসাথে সীমিত সংখ্যক সংযোগ রাখতে পারে। তাই অংশ ৮-এ pool.
  2. SQL প্রস্তুত করা। ? হলো parameterized query-র placeholder — ছাপানো ফর্মের ফাঁকা ঘর। ঘরে কী বসবে তা এখনো জানা নেই, কিন্তু ফর্মের লেখা আর বদলাবে না।
  3. মান বসানো (bind). setString(1, phone) — প্রথম ?-এ ফোন নম্বর। সংখ্যার জন্য setLong, তারিখের জন্য setObject(i, localDate). JDBC-তে index শুরু ১ থেকে — Java-র array-র মতো ০ থেকে নয়, এটি নতুনদের প্রিয় ভুল।
  4. পাঠানো। SELECT-এর জন্য executeQuery(), যা ResultSet দেয়। INSERT/UPDATE/DELETE-এর জন্য executeUpdate(), যা কয়টি সারি বদলেছে সেই সংখ্যা দেয় (অংশ ৬)।
  5. সারি সারি পড়া। ResultSet একটি cursor — শুরুতে প্রথম সারির আগে থাকে। rs.next() এক সারি এগোয় আর জানায় সারি আছে কি না। একটি ফল আশা করলে if, অনেক হলে while.
  6. record-এ রূপান্তর। কলামের নাম বা নম্বর দিয়ে মান পড়ে একটি record (পর্ব ২৫) বানানো। এর পর বাকি প্রোগ্রাম আর JDBC চেনে না — শুধু Customer. সতর্কতা: কলাম NULL হলে getLong ফেরত দেয় 0; NULL হতে পারে এমন কলামে rs.getObject("col", Long.class) ব্যবহার করুন।
  7. উল্টো ক্রমে বন্ধ। try-with-resources (পর্ব ২৯) ঠিক এটাই করে, কারণ Connection, PreparedStatement, ResultSet — তিনটিই AutoCloseable. বন্ধ না করলে ডেটাবেসের সংযোগ ফুরিয়ে যায়, আর কয়েক ঘণ্টা পর অ্যাপ রহস্যজনকভাবে আটকে যায়।

stepper-এর পুরো কোড একটি method-এ। নিচের কোডের জন্য একটি সত্যিকারের ডেটাবেস আর driver লাগে, তাই এখানে চালানো যাবে না:

public Optional<Customer> findByPhone(String phone) {
    String sql = "SELECT id, name, phone, district FROM customers WHERE phone = ?";
    try (Connection con = dataSource.getConnection();
         PreparedStatement ps = con.prepareStatement(sql)) {
        ps.setString(1, phone);
        try (ResultSet rs = ps.executeQuery()) {
            if (!rs.next()) return Optional.empty();
            return Optional.of(new Customer(
                    rs.getLong("id"), rs.getString("name"),
                    rs.getString("phone"), rs.getString("district")));
        }
    } catch (SQLException e) {
        throw new DataAccessException("findByPhone failed: " + phone, e);
    }
}

ছবি ৪৭.১-এর ডেটা থাকলে findByPhone("01811000002") দেবে Optional[Customer[id=2, name=করিম, phone=01811000002, district=রাজশাহী]], আর অচেনা নম্বরে Optional.empty — “না পাওয়া” স্বাভাবিক ফল, তাই Optional (পর্ব ৩৬), exception নয়। লক্ষ করুন SELECT * নয়, কলামের নাম আলাদা করে লেখা — টেবিলে নতুন কলাম যোগ হলেও এই কোড একই থাকবে, আর অপ্রয়োজনীয় ডেটা নেটওয়ার্কে আসবে না। SQLException একটি checked exception; এখানে তাকে নিজের unchecked exception-এ cause সহ মুড়ে দেওয়া হয়েছে — পর্ব ২৯-এর exception chaining.

৫ · SQL injection — যখন ইনপুট নির্দেশ হয়ে যায়

JDBC-তে PreparedStatement-এর একটি পুরোনো ভাই আছে: Statement, যাকে পুরো SQL একটি String হিসেবে দিতে হয়। নতুনরা (আর AI) তাই লেখেন "... WHERE phone = '" + phone + "'". দেখতে নিরীহ। কিন্তু এখানে ব্যবহারকারীর লেখা সরাসরি SQL-এর অংশ হয়ে যাচ্ছে — আর ব্যবহারকারী যদি এমন কিছু লেখেন যা SQL-এর অর্থ বদলে দেয়? এই দুর্বলতার নাম SQL injection. নিচের প্রোগ্রাম কোনো ডেটাবেসে যায় না; শুধু দেখায় দুই পদ্ধতিতে ডেটাবেসের কাছে কী লেখা পৌঁছাত:

import java.util.List;

record Query(String sql, List<Object> params) {}

class Main {
    // ✗ ইনপুট সরাসরি SQL লেখার ভেতরে জোড়া
    static String unsafe(String phone) {
        return "SELECT id, name FROM customers WHERE phone = '" + phone + "'";
    }
    // ✓ SQL-এর কাঠামো স্থির, ইনপুট যায় আলাদা তালিকায়
    static Query safe(String phone) {
        return new Query("SELECT id, name FROM customers WHERE phone = ?", List.of(phone));
    }
    public static void main(String[] args) {
        for (String input : List.of("01711000001", "' OR '1'='1")) {
            System.out.println("ইনপুট   : " + input);
            System.out.println("জোড়া SQL: " + unsafe(input));
            Query q = safe(input);
            System.out.println("নিরাপদ  : " + q.sql());
            System.out.println("params  : " + q.params());
            System.out.println("---");
        }
    }
}

দ্বিতীয় ইনপুটে জোড়া SQL দাঁড়ায় ... WHERE phone = '' OR '1'='1'. ইনপুটের প্রথম ' চিহ্নটি SQL-এর খোলা উদ্ধৃতি বন্ধ করে দিয়েছে, আর বাকি লেখা হয়ে গেছে নতুন শর্ত। '1'='1' সবসময় সত্য, তাই এক ক্রেতার বদলে সব ক্রেতার নাম-ফোন ফেরত আসত। নিরাপদ পথে SQL দুইবারই হুবহু এক — WHERE phone = ? — আর ইনপুট থাকে আলাদা তালিকায় (params : [' OR '1'='1]). ডেটাবেস তখন খুঁজবে এমন ক্রেতা, যাঁর ফোন নম্বর আক্ষরিক অর্থে ' OR '1'='1 — কেউ নেই, শূন্য সারি। (Query record এখানে শুধু ধারণা দেখানোর জন্য; আসল কাজটা করে PreparedStatement-এর setString.)

  1. ছাঁচ"…'" + x + "'"জোড়া
  2. সাধারণ ইনপুট017…ঠিক চলে
  3. উদ্ধৃতিসহ ইনপুট' OR …অর্থ বদলায়
  4. preparephone = ?কাঠামো আগে
  5. bindsetStringশুধুই ডেটা
ধাপ ১Statement + জোড়া লাগানো
"SELECT ... WHERE phone = '" + input + "'"
ইনপুট আর কোড একই String-এ মিশে যায়
ডেটাবেস বুঝতে পারে না কোনটা কার লেখা
ধাপ ২input = 01711000001
WHERE phone = '01711000001'
→ ১টি সারি, যেমনটা চাওয়া হয়েছিল
সব টেস্ট পাস — তাই সমস্যা চোখে পড়ে না
ধাপ ৩input = ' OR '1'='1
WHERE phone = '' OR '1'='1'
       ↑ খালি লেখা  ↑ নতুন শর্ত, সবসময় সত্য
→ সব ক্রেতার তথ্য ফেরত
ধাপ ৪con.prepareStatement(sql)
SELECT ... WHERE phone = ?
ডেটাবেস এই কাঠামো আগেই পড়ে নেয়:
একটি কলাম, একটি তুলনা, একটি মানের জায়গা
ধাপ ৫ps.setString(1, input)
parameter 1 = "' OR '1'='1"  (শুধু একটি লেখা)
phone কলামে কি ঠিক এই লেখা আছে? না
→ ০টি সারি; কাঠামো বদলানোর কোনো পথ নেই
  1. সমস্যার মূল: কোড আর ডেটা এক String-এ। ডেটাবেস পুরো লেখাটি একবারে পায় আর SQL হিসেবে পড়ে। কোন অংশ প্রোগ্রামারের লেখা আর কোন অংশ ব্যবহারকারীর, সেই সীমানা আর থাকে না।
  2. সাধারণ ইনপুটে সব ঠিক। এজন্যই এই bug এত বিপজ্জনক — সাধারণ ব্যবহারে, demo-তে, টেস্টে কোনো সমস্যা দেখা যায় না। ব্যবহারকারীর নামে একটি উদ্ধৃতি চিহ্ন (যেমন D'Costa) এলে প্রথম লক্ষণ দেখা দেয় — SQL syntax error.
  3. উদ্ধৃতি চিহ্ন সীমানা ভেঙে দেয়। ইনপুটের ' SQL-এর লেখাকে আগেভাগে বন্ধ করে, আর বাকিটা নতুন SQL হয়ে যায়। এখানে শুধু সব সারি ফেরত আসছে; একই কৌশলে লগইনের পাসওয়ার্ড যাচাই এড়ানো বা অন্য টেবিলের ডেটা পড়াও সম্ভব — তাই এটি নিরাপত্তার সবচেয়ে পরিচিত দুর্বলতাগুলোর একটি।
  4. PreparedStatement: কাঠামো আগে, আলাদাভাবে। SQL-এ ? রেখে ডেটাবেসকে আগে জানিয়ে দেওয়া হয় query-র আকার কী। এরপর যত অদ্ভুত ইনপুটই আসুক, এই আকার আর বদলায় না।
  5. মান যায় শুধু ডেটা হিসেবে। driver মানটিকে আলাদা parameter হিসেবে পাঠায় (অথবা নিজেই নিরাপদভাবে escape করে) — ডেটাবেস কখনো সেটিকে SQL হিসেবে পড়ে না। নিজে replace("'", "''") দিয়ে escape করার চেষ্টা করবেন না; ডেটাবেসভেদে নিয়ম আলাদা, আর ভুল হওয়ার সুযোগ অনেক।

এই দুর্বলতা নিয়ে xkcd-র ২০০৭ সালের বিখ্যাত কমিক “Exploits of a Mom”-এ এক মা ছেলের নাম রাখেন Robert'); DROP TABLE Students;--, আর স্কুলের সফটওয়্যার ছাত্রদের টেবিলই মুছে ফেলে। সেই থেকে প্রোগ্রামারদের মুখে মুখে “Little Bobby Tables”. কমিকের শেষ লাইনটাই শিক্ষা: ইনপুটকে পরিষ্কার করার চেষ্টার চেয়ে সঠিক পথ হলো ইনপুটকে কখনো কোড হতে না দেওয়া।

মূল ধারণা

নিয়ম একটাই, ব্যতিক্রম নেই: বাইরে থেকে আসা কোনো মান SQL-এর লেখায় জুড়বেন না — সবসময় ? আর setXxx. একটি সীমা মনে রাখুন: ? শুধু মানের জায়গায় বসে — টেবিল বা কলামের নাম, ORDER BY-এর দিক (ASC/DESC) এভাবে বসানো যায় না। সেগুলো ব্যবহারকারী বাছলে আগে থেকে ঠিক করা একটি তালিকা (allow-list) থেকে মিলিয়ে নিন। নামের অংশ দিয়ে খোঁজার জন্য WHERE name LIKE ? লিখে মান দিন "%" + term + "%".

৬ · লেখা — executeUpdate, generated key, batch আর SQLException

INSERT, UPDATE, DELETE চালায় executeUpdate(), আর ফেরত দেয় একটি int — কয়টি সারি প্রভাবিত হলো। এই সংখ্যা উপেক্ষা করবেন না: UPDATE ... WHERE id = ? যদি 0 দেয়, তার মানে ওই id-র সারিই নেই, আর exception ছাড়াই কিছু ঘটেনি। নতুন সারির id ডেটাবেস বানালে (IDENTITY) সেটা ফেরত পেতে চাইতে হয় generated key:

public Customer save(String name, String phone, String district) {
    String sql = "INSERT INTO customers (name, phone, district) VALUES (?, ?, ?)";
    try (Connection con = dataSource.getConnection();
         PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
        ps.setString(1, name);
        ps.setString(2, phone);
        ps.setString(3, district);
        ps.executeUpdate();                                   // ফেরত দেয় 1
        try (ResultSet keys = ps.getGeneratedKeys()) {
            keys.next();
            return new Customer(keys.getLong(1), name, phone, district);
        }
    } catch (SQLException e) {
        throw new DataAccessException("save failed: " + phone, e);
    }
}

batch — হাজার সারি, একবারে

একটি CSV থেকে দশ হাজার প্রোডাক্ট ঢোকাতে হবে। লুপে প্রতিবার executeUpdate() মানে দশ হাজারবার নেটওয়ার্কে যাওয়া-আসা। batch update-এ প্রতিটি সারির মান বসিয়ে addBatch(), শেষে একবার executeBatch(), যা প্রতিটি সারির ফলের একটি int[] দেয়। সাধারণত পুরো batch একটি transaction-এ রাখা হয় (পরের অংশ), যাতে মাঝপথে ব্যর্থ হলে অর্ধেক ঢোকানো ডেটা না থাকে:

con.setAutoCommit(false);
try (PreparedStatement ps = con.prepareStatement(
        "INSERT INTO products (name, price) VALUES (?, ?)")) {
    for (Product p : products) {
        ps.setString(1, p.name());
        ps.setLong(2, p.price());
        ps.addBatch();
    }
    int[] counts = ps.executeBatch();      // প্রতিটি সারির ফল
    con.commit();
} catch (SQLException e) {
    con.rollback();
    throw e;
}

SQLException আর SQLState

ডেটাবেসের প্রায় সব সমস্যা আসে SQLException হয়ে — ভুল SQL, সংযোগ বিচ্ছিন্ন, নিয়ম ভাঙা। বার্তা ডেটাবেসভেদে আলাদা, তাই কোড দিয়ে সিদ্ধান্ত নিতে চাইলে দেখুন getSQLState() — পাঁচ অক্ষরের মানসম্মত SQLState, যার প্রথম দুই অক্ষর শ্রেণি বলে:

শ্রেণিমানেউদাহরণ ও করণীয়
08সংযোগের সমস্যা08001 — driver নেই বা server-এ পৌঁছানো যায়নি; config যাচাই, পরে আবার চেষ্টা
23নিয়ম (constraint) ভাঙাএকই ফোন নম্বর দুবার (UNIQUE) — ব্যবহারকারীকে “এই নম্বর আগেই আছে” বলা
42SQL-এ ভুল বা অনুমতি নেইটেবিলের নামে বানান ভুল — প্রোগ্রামের bug, ঠিক করুন
40transaction rollback হয়েছে40001 — একসাথে চলা লেনদেনের সংঘাত; পুরো transaction আবার চালানো যায়

ডেটাবেস কোম্পানির নিজস্ব সংখ্যা পাওয়া যায় getErrorCode() দিয়ে। আর একটি সুবিধা: পর্ব ২৯-এর নিয়মে SQLException নিচের স্তরেই ধরে নিজের ভাষার exception-এ রূপান্তর করুন (DuplicatePhoneException), যাতে ওপরের ব্যবসায়িক কোড SQL-এর খুঁটিনাটি না জানে। Spring ঠিক এটাই করে — সব SQLException-কে নিজের unchecked DataAccessException-পরিবারে অনুবাদ করে।

৭ · transaction — সব নয়তো কিছুই নয়

রহিম তাঁর ওয়ালেট থেকে করিমকে ১০০০ টাকা পাঠাবেন। ডেটাবেসে এটা দুটি UPDATE: রহিমের ব্যালেন্স থেকে বাদ, করিমের ব্যালেন্সে যোগ। প্রথমটি সফল হয়ে দ্বিতীয়টির আগে কিছু ভেঙে পড়লে টাকা উধাও। এখানে লাগে transaction — কয়েকটি কাজের একটি দল, যা ডেটাবেস একক হিসেবে দেখে: হয় সবগুলো স্থায়ী হবে (commit), নয়তো সবগুলো বাতিল (rollback).

JDBC-তে নতুন connection থাকে auto-commit অবস্থায়: প্রতিটি statement নিজেই একটি transaction, চলামাত্র commit. কয়েকটি statement একসাথে বাঁধতে হলে setAutoCommit(false) দিয়ে শুরু করুন:

1con.setAutoCommit(false);2try {3    debit.executeUpdate();   // রহিম -10004    credit.executeUpdate();  // করিম +10005    con.commit();6} catch (SQLException e) {7    con.rollback();8    throw e;9}
ডেটাবেসের ভেতরে
রহিম (এই transaction দেখে)
করিম (এই transaction দেখে)
অন্য connection দেখে
  1. auto-commit বন্ধ — transaction শুরু। এখন থেকে এই connection-এর প্রতিটি বদল একটি খোলা transaction-এর অংশ, commit() বা rollback() না হওয়া পর্যন্ত।
  2. debit সফল। রহিমের সারিতে ৪০০০. কিন্তু এই বদল এখনো শুধু এই transaction দেখে — অন্য connection (যেমন রহিমের ব্যালেন্স দেখানো অ্যাপ) এখনো ৫০০০ দেখে। এটাই isolation.
  3. credit ব্যর্থ। করিমের ওয়ালেট id ভুল, অথবা ঠিক এই মুহূর্তে নেটওয়ার্ক কেটে গেল — SQLException. commit-এর লাইনে আর পৌঁছানো হলো না।
  4. rollback — সব ফিরে আগের অবস্থায়। catch-এ rollback() রহিমের debit বাতিল করে। কেউ কখনো ৪০০০ দেখেনি। connection-ই যদি মারা যায় আর rollback ডাকাও না যায়? ডেটাবেস নিজে অসমাপ্ত transaction বাতিল করে — commit না হওয়া কিছুই টিকে থাকে না।
  5. আবার চেষ্টা — এবার দুটিই সফল। debit আর credit দুটিই চলল, কিন্তু এখনো অস্থায়ী। এই মুহূর্তে সার্ভার crash করলেও ডেটা নিরাপদ — আগের অবস্থায় থাকবে।
  6. commit — একসাথে স্থায়ী। দুটি বদল একই মুহূর্তে সবার কাছে দৃশ্যমান হয়, আর ডেটাবেস নিশ্চিত করে যে এরপর বিদ্যুৎ চলে গেলেও তা হারাবে না। মাঝামাঝি কোনো অবস্থা কেউ কখনো দেখে না।

বাস্তবের পূর্ণ কোডে আরও দুটি সতর্কতা আছে: পর্যাপ্ত ব্যালেন্স যাচাই আর executeUpdate()-এর ফেরত-সংখ্যা যাচাই:

void transfer(long fromId, long toId, long amount)
        throws SQLException, InsufficientBalanceException {
    String debit  = "UPDATE wallets SET balance = balance - ? WHERE id = ? AND balance >= ?";
    String credit = "UPDATE wallets SET balance = balance + ? WHERE id = ?";
    try (Connection con = dataSource.getConnection()) {
        con.setAutoCommit(false);
        try (PreparedStatement d = con.prepareStatement(debit);
             PreparedStatement c = con.prepareStatement(credit)) {
            d.setLong(1, amount); d.setLong(2, fromId); d.setLong(3, amount);
            if (d.executeUpdate() != 1) throw new InsufficientBalanceException("wallet " + fromId);
            c.setLong(1, amount); c.setLong(2, toId);
            if (c.executeUpdate() != 1) throw new SQLException("wallet " + toId + " not found");
            con.commit();
        } catch (SQLException | InsufficientBalanceException | RuntimeException e) {
            con.rollback();
            throw e;
        }
    }
}

debit-এর WHERE ... AND balance >= ? লক্ষ করুন: ব্যালেন্স যাচাই আর বাদ দেওয়া একই statement-এ, তাই আগে SELECT করে পরে UPDATE করার মাঝের ফাঁকে অন্য কেউ ঢুকে পড়তে পারে না — পর্ব ৪০-এর check-then-act race condition এভাবে ডেটাবেসেই বন্ধ। 0 সারি বদলালে মানে ব্যালেন্স কম। (InsufficientBalanceException এখানে নিজের বানানো একটি checked exception; কোডটি চালাতে ডেটাবেস লাগবে।)

ACID — transaction-এর চারটি প্রতিশ্রুতি

Atomicity

সবগুলো, নয়তো একটিও না। ওপরের stepper-এর ধাপ ৪.

অবিভাজ্য

Consistency

transaction শেষে সব নিয়ম (CHECK, foreign key) মেনে চলে। ঋণাত্মক ব্যালেন্স কখনো commit হয় না।

নিয়ম অটুট

Isolation

একসাথে চলা transaction-গুলো একে অপরের অর্ধেক কাজ দেখে না (কতটা কঠোর, তা isolation level ঠিক করে)।

আলাদা

Durability

commit হওয়ার পর crash হলেও ডেটা থাকে — ডেটাবেস আগে log-এ লিখে তারপর “হয়েছে” বলে।

টেকসই

চারটি একসাথে ACID. Isolation সবচেয়ে সূক্ষ্ম: পুরো কঠোর হলে ধীর, ঢিলে হলে অদ্ভুত ফল। con.setTransactionIsolation(...) দিয়ে isolation level বদলানো যায় — READ_COMMITTED (PostgreSQL-এর ডিফল্ট), REPEATABLE_READ (MySQL InnoDB-র ডিফল্ট), SERIALIZABLE (সবচেয়ে কঠোর)। শুরুতে ডিফল্ট রাখুন, আর টাকার হিসাবে ওপরের মতো শর্তসহ UPDATE লিখুন।

৮ · কোড সাজানো — Repository pattern আর connection pool

প্রতিটি ব্যবসায়িক method-এর ভেতরে SQL আর ResultSet ছড়িয়ে থাকলে কী হয়? ডেটাবেস বদলানো কঠিন, ব্যবসার নিয়ম টেস্ট করতে ডেটাবেস লাগে, আর একই SQL পাঁচ জায়গায় পাঁচভাবে লেখা। সমাধান পর্ব ৪৮-এর একটি design pattern: Repository pattern — ডেটা রাখা-আনার সব কাজ একটি interface-এর পেছনে, যা ব্যবসার ভাষায় কথা বলে (findByPhone, save), SQL-এর ভাষায় নয়। একই ধারণার পুরোনো নাম DAO (Data Access Object). নিচে interface আর একটি Map-ভিত্তিক implementation — JDBC implementation (অংশ ৪ আর ৬-এর method দুটি) ঠিক এর জায়গা নেবে, আর বাকি কোড এক অক্ষরও বদলাবে না:

import java.util.*;
record Customer(long id, String name, String phone, String district) {}
interface CustomerRepository {
    Customer save(String name, String phone, String district);
    Optional<Customer> findByPhone(String phone);
}
class InMemoryCustomerRepository implements CustomerRepository {
    private final Map<String, Customer> byPhone = new LinkedHashMap<>();
    public Customer save(String name, String phone, String district) {
        var c = new Customer(byPhone.size() + 1, name, phone, district);
        byPhone.put(phone, c);
        return c;
    }
    public Optional<Customer> findByPhone(String phone) {
        return Optional.ofNullable(byPhone.get(phone));
    }
}
class Main {
    static String register(CustomerRepository repo, String name, String phone, String district) {
        if (repo.findByPhone(phone).isPresent()) return "বাতিল: " + phone + " আগেই আছে";
        return "যোগ হলো: " + repo.save(name, phone, district);
    }
    public static void main(String[] args) {
        CustomerRepository repo = new InMemoryCustomerRepository();
        System.out.println(register(repo, "রহিম", "01711000001", "ঢাকা"));
        System.out.println(register(repo, "করিম", "01811000002", "রাজশাহী"));
        System.out.println(register(repo, "রহিমা", "01711000001", "খুলনা"));
        System.out.println(repo.findByPhone("01999999999").map(Customer::name).orElse("নেই"));
    }
}

আউটপুট চার লাইন: যোগ হলো: Customer[id=1, name=রহিম, phone=01711000001, district=ঢাকা], যোগ হলো: Customer[id=2, name=করিম, phone=01811000002, district=রাজশাহী], বাতিল: 01711000001 আগেই আছে, নেই. ব্যবসার নিয়মের method register ব্যবসার নিয়ম (“একই নম্বর দুবার নয়”) জানে, কিন্তু Map, SQL বা JDBC কিছুই জানে না। বাস্তবে main-এ শুধু একটি লাইন বদলাবে: new JdbcCustomerRepository(dataSource). আর টেস্টে এই Map-সংস্করণ দিয়ে ব্যবসার নিয়ম মিলিসেকেন্ডে যাচাই করা যায় — পর্ব ৪৭-এর JUnit-এ। Map-এর key এখানে ফোন নম্বর, তাই খোঁজা সরাসরি; LinkedHashMap নেওয়া হয়েছে যাতে ঢোকানোর ক্রম নিশ্চিত থাকে। একটি সতর্কতা: আসল ডেটাবেসে “আগে খুঁজে তারপর ঢোকানো” দুজন একসাথে করলে দুটিই পাস করতে পারে — শেষ পাহারাদার তাই phone-এর UNIQUE নিয়ম, যার লঙ্ঘন আসে SQLState 23 হয়ে। (অনলাইন runner-এর জন্য কোনো class public নয়।)

connection pool — সংযোগ ধার নেওয়া আর ফেরত দেওয়া

প্রতিটি অনুরোধে নতুন connection খোলা মানে প্রতিবার নেটওয়ার্ক handshake, পাসওয়ার্ড যাচাই, সেশন তৈরি — query-র চেয়ে বেশি সময় যেতে পারে সংযোগেই। connection pool অ্যাপ শুরুর সময় কয়েকটি connection খুলে রাখে আর ধার দেয়। Java-তে সবচেয়ে প্রচলিত pool HikariCP, যা Spring Boot 2.0 থেকে ডিফল্ট। pool নিজেই একটি DataSource, তাই Repository-র কোড বদলায় না — এটাই DriverManager-এর বদলে DataSource নেওয়ার সুবিধা:

var config = new HikariConfig();
config.setJdbcUrl(System.getenv("DB_URL"));
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(10);
DataSource dataSource = new HikariDataSource(config);
var repo = new JdbcCustomerRepository(dataSource);
  1. শুরু৩টি খোলাঅলস
  2. ধারgetConnection()A নিল
  3. সব ব্যস্তA, B, Cঅলস ০
  4. অপেক্ষাDtimeout পর্যন্ত
  5. ফেরতclose()D পেল
ধাপ ১অ্যাপ চালু হলো (pool আকার ৩)
অলস : [c1] [c2] [c3]
ব্যস্ত: —
সংযোগের খরচ একবারই, শুরুতে
ধাপ ২অনুরোধ A
অলস : [c2] [c3]
ব্যস্ত: c1 → A
নতুন সংযোগ খোলা হয়নি, শুধু ধার
ধাপ ৩অনুরোধ B, C
অলস : —
ব্যস্ত: c1 → A, c2 → B, c3 → C
ধাপ ৪অনুরোধ D
অলস : —
অপেক্ষায়: D
নির্দিষ্ট সময়ে না পেলে SQLException
(HikariCP-র connectionTimeout)
ধাপ ৫A-র try-with-resources শেষ
A: con.close()  → আসলে বন্ধ নয়, pool-এ ফেরত
ব্যস্ত: c1 → D, c2 → B, c3 → C
  1. শুরুতে কয়েকটি সংযোগ খোলা। নেটওয়ার্ক আর পাসওয়ার্ড যাচাইয়ের খরচ একবারই দেওয়া হয়।
  2. ধার নেওয়া প্রায় বিনা খরচে। dataSource.getConnection() এখন একটি অলস সংযোগ হাতে দেয় — মাইক্রোসেকেন্ডের ব্যাপার।
  3. সব সংযোগ ব্যস্ত। pool-এর আকার সীমিত, কারণ ডেটাবেস server-ও সীমিত সংখ্যক সংযোগ সামলাতে পারে। বড় pool মানেই দ্রুত নয় — ডেটাবেসের CPU আর ডিস্ক তো একই।
  4. পরের অনুরোধ অপেক্ষা করে। নির্দিষ্ট সময়ের মধ্যে কেউ ফেরত না দিলে exception. কোড যদি কোথাও close() ভুলে যায়, সংযোগ আর কখনো ফেরে না — একে বলে connection leak, আর এই ধাপেই অ্যাপ আটকে যায়। পর্ব ৪২-এর virtual thread দিয়ে হাজারো অনুরোধ একসাথে চালালেও সীমা এই pool-ই।
  5. close() মানে ফেরত। pool-এর দেওয়া connection-এর close() নেটওয়ার্ক সংযোগ বন্ধ করে না, pool-এ ফিরিয়ে দেয়। তাই pool থাকলেও try-with-resources বাধ্যতামূলক — বরং আরও বেশি জরুরি।

৯ · বাস্তব প্রজেক্টে — N+1, টেস্টিং, migration, নিরাপত্তা আর উচ্চস্তরের টুল

N+1 query সমস্যা

অর্ডারের তালিকা দেখাতে হবে, প্রতিটির পাশে ক্রেতার নাম। সহজ কোড:

List<Order> orders = orderRepo.findAll();                  // ১টি query
for (Order o : orders) {
    Customer c = customerRepo.findById(o.customerId());     // প্রতিটি অর্ডারে আরও ১টি
    System.out.println(o.id() + " " + c.name());
}

১০০টি অর্ডার হলে ডেটাবেসে যায় ১০১টি query — একে বলে N+1 query problem. নিজের ল্যাপটপে ১০টি অর্ডারে টের পাবেন না; production-এ দশ হাজার অর্ডারে পেজ খুলতে কয়েক সেকেন্ড। সমাধান অংশ ৩-এর JOIN — একটি query-তে অর্ডার আর নাম একসাথে — অথবা সব customerId নিয়ে একবার WHERE id IN (...). পর্ব ৫১-এ দেখবেন ORM-এ এই সমস্যা আরও লুকিয়ে আসে, কারণ সেখানে order.getCustomer() দেখতে সাধারণ getter, কিন্তু ভেতরে চুপচাপ query চালাতে পারে।

টেস্টিং আর migration

Repository-র JDBC implementation-ও টেস্ট করতে হবে — SQL-এ বানান ভুল তো compiler ধরে না। দুটি প্রচলিত পথ: H2-র মতো in-memory database (jdbc:h2:mem:test), যা টেস্ট শুরুতে তৈরি হয় আর শেষে মুছে যায়; অথবা Testcontainers, যা Docker-এ সত্যিকারের PostgreSQL চালিয়ে টেস্ট করে। H2 দ্রুত, কিন্তু SQL-এর খুঁটিনাটি PostgreSQL-এর সাথে পুরো মেলে না; Testcontainers ধীর, কিন্তু production-এর মতো — পর্ব ৪৭-এর JUnit দিয়েই দুটি চালানো যায়।

schema-ও কোডের মতো বদলায় — নতুন কলাম, নতুন টেবিল। হাতে হাতে প্রতিটি server-এ ALTER TABLE চালালে কোথায় কোন সংস্করণ, কেউ জানে না। database migration টুল — Flyway বা Liquibase — এই বদলগুলোকে ক্রমিক ফাইলে রাখে (V1__create_customers.sql, V2__add_orders.sql), git-এ কোডের সাথে, আর অ্যাপ শুরুর সময় যেগুলো এখনো চলেনি শুধু সেগুলো চালায়। কোনটা চলেছে তা Flyway নিজের একটি টেবিলে লিখে রাখে।

নিরাপত্তা — credential আর অনুমতি

  • পাসওয়ার্ড কোডে নয়: "jdbc:...", "root", "123456" git-এ গেলে চিরকালের জন্য ফাঁস। URL, ব্যবহারকারী, পাসওয়ার্ড পড়ুন environment variable বা secret manager থেকে, যেমন ওপরের System.getenv("DB_PASSWORD").
  • লগে নয়: config বা JDBC URL লগ করবেন না — কিছু URL-এ পাসওয়ার্ড থাকে (?password=...)। exception-এর বার্তায়ও নয়।
  • ন্যূনতম অনুমতি: least privilege — অ্যাপের ডেটাবেস ব্যবহারকারী শুধু নিজের টেবিলে SELECT/INSERT/UPDATE/DELETE পাবে; DROP বা অন্যের টেবিলে নয়, আর কখনো root/postgres superuser নয়। migration চালাতে আলাদা ব্যবহারকারী। তাহলে কোথাও injection ফাঁক থেকে গেলেও ক্ষতি সীমিত।

JDBC-র ওপরের স্তর — কোনটা কখন

কাঁচা JDBC-তে একটি SELECT-এর জন্য দশ-পনেরো লাইন লাগে। তাই বাস্তবে প্রায় সবাই এর ওপরে কোনো স্তর ব্যবহার করেন — কিন্তু সবগুলোর নিচে JDBC-ই চলে, তাই এই পর্বের ধারণাগুলো সবখানে কাজে লাগবে:

পদ্ধতিআপনি লেখেনসুবিধাদাম
কাঁচা JDBCSQL + সব resource-এর কোডপূর্ণ নিয়ন্ত্রণ, কোনো লাইব্রেরি নয়অনেক boilerplate, ভুলের সুযোগ বেশি
Spring JdbcTemplate / JdbcClientSQL + সারি থেকে object-এর নিয়মresource, exception অনুবাদ, transaction Spring সামলায়Spring-এর ওপর নির্ভর; SQL এখনো String
jOOQJava method দিয়ে SQL (schema থেকে তৈরি class)SQL-এর ভুল compile-এর সময় ধরা পড়েcode generation, বাণিজ্যিক ডেটাবেসে লাইসেন্স
JPA / Hibernate (ORM)annotation-যুক্ত class; SQL প্রায় লিখতেই হয় নাCRUD খুব দ্রুত, object-এর সম্পর্ক সহজলুকোনো query (N+1), শেখার খাড়া পথ, জটিল রিপোর্টে কষ্ট

Spring Framework 6.1-এ আসা JdbcClient দিয়ে অংশ ৪-এর পুরো method প্রায় এক লাইনে নেমে আসে — ?, resource বন্ধ, আর record-এ রূপান্তর সব নিজে করে:

List<Customer> dhaka = jdbcClient
        .sql("SELECT id, name, phone, district FROM customers WHERE district = ?")
        .param("ঢাকা")
        .query(Customer.class)
        .list();

Spring Boot কীভাবে এই jdbcClient আর DataSource নিজে বানিয়ে আপনার class-এ দিয়ে দেয় (dependency injection), তা পর্ব ৫০-এ; JPA আর Hibernate-এর প্রথম দেখা পর্ব ৫১-এর REST API-তে।

C/C++/Python জানলে

Python-এর sqlite3 module একই চুক্তি মানে (DB-API, PEP 249): connection, cursor, ? placeholder, commit()/rollback() — আর with মানে try-with-resources. পার্থক্য: Python-এ সব exception unchecked, আর JDBC-র মতো এক মানসম্মত driver ব্যবস্থা নেই, প্রতিটি লাইব্রেরি একই নিয়মে আলাদা module. C/C++-এ SQLite-এর C API-তে একই ধারণা sqlite3_prepare_v2 আর sqlite3_bind_text নামে। Injection-এর পুরো আলোচনা দেখুন Python-এর নিরাপদ কোড পর্বে।

পরীক্ষা ও ইন্টারভিউতে
  • Statement আর PreparedStatement-এর পার্থক্য? — PreparedStatement-এ SQL-এর কাঠামো আগে, মান ?-এ আলাদা যায়: SQL injection ঠেকায়, টাইপ ঠিক রাখে, আর একই SQL বারবার চালাতে দক্ষ।
  • execute, executeQuery, executeUpdate? — SELECT-এ executeQuery (ResultSet), INSERT/UPDATE/DELETE-এ executeUpdate (প্রভাবিত সারির সংখ্যা), আর ফলের ধরন অজানা হলে execute (boolean).
  • ACID কী? — Atomicity, Consistency, Isolation, Durability — transaction-এর চারটি প্রতিশ্রুতি।
  • DriverManager না DataSource? — DriverManager প্রতিবার নতুন সংযোগ খোলে; DataSource একটি interface, যার পেছনে connection pool থাকতে পারে — বাস্তব অ্যাপে DataSource.
  • N+1 সমস্যা কী? — একটি তালিকা আনতে ১টি query, তারপর প্রতিটি উপাদানের জন্য আরও ১টি; সমাধান JOIN বা IN.

১০ · AI-এর যুগে — AI-এর লেখা ডেটাবেস কোড রিভিউ করুন

AI-কে “Java দিয়ে ডেটাবেস থেকে ইউজার খোঁজার কোড লেখো” বললে প্রায়ই যা আসে, তা চলে — আর এই পর্বের প্রায় প্রতিটি নিয়ম ভাঙে। কারণ ইন্টারনেটের পুরোনো টিউটোরিয়ালে এভাবেই লেখা, আর AI সেগুলো থেকেই শিখেছে। নিচের ভুলগুলো চিনে রাখুন:

AI যা করেকেন সমস্যাআপনি যা চাইবেন
Statement + "... '" + input + "'"SQL injection; নামের উদ্ধৃতিতেই ভাঙেPreparedStatement, ?, setXxx
connection খুলে close() নেই, বা শুধু শেষে হাতে close()exception হলে সংযোগ ফেরে না → pool ফুরায়try-with-resources-এ তিনটিই
দুটি সম্পর্কিত UPDATE, কোনো transaction নেইমাঝপথে ব্যর্থ হলে অর্ধেক বদল থেকে যায়setAutoCommit(false), commit, catch-এ rollback
getConnection(url, "root", "password")credential git-এ; superuser দিয়ে চলাenvironment variable; আলাদা কম-অনুমতির ব্যবহারকারী
SELECT * সবখানেঅপ্রয়োজনীয় ডেটা; কলাম বদলালে ভাঙেদরকারি কলামের নাম আলাদা করে
লুপের ভেতরে queryN+1 — বড় ডেটায় খুব ধীরJOIN, IN, বা batch

AI-কে ডেটাবেস কোড লেখাতে বা রিভিউ করাতে এই কাঠামো ব্যবহার করুন:

আমি Java 21 শিখছি, JDBC, PreparedStatement আর transaction জানি। কাজ: [যেমন — জেলা অনুযায়ী ক্রেতা খোঁজা আর এক ওয়ালেট থেকে অন্যটিতে টাকা পাঠানো] টেবিল: [CREATE TABLE লাইনগুলো এখানে দাও] শর্ত: ১. শুধু PreparedStatement আর ? placeholder; কোনো String জোড়া লাগানো SQL নয়। ২. Connection, PreparedStatement, ResultSet — সব try-with-resources-এ। ৩. একাধিক বদল একসাথে হলে transaction: setAutoCommit(false), commit, catch-এ rollback. ৪. SELECT * নয়; দরকারি কলাম আলাদা করে লেখো, ফল record-এ। ৫. লুপের ভেতরে কোনো query নয় — JOIN বা IN ব্যবহার করো। ৬. URL, ব্যবহারকারী, পাসওয়ার্ড System.getenv থেকে; লগে কখনো নয়। ৭. SQL আলাদা Repository class-এ, interface-এর পেছনে। ৮. শেষে বলো: কোন method কয়টি query চালায়, আর কোন জায়গায় SQLException কীভাবে সামলানো হলো।

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

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

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১১৯৭০ সালের একটি গবেষণা-প্রবন্ধ কীভাবে আজকের প্রায় প্রতিটি ব্যাংক, দোকান আর সরকারি ডেটাবেসের ভিত্তি হলো? SQL এল কোথা থেকে?

Codd-এর আগে। ১৯৬০-এর দশকের ডেটাবেস — যেমন IBM-এর IMS — ছিল hierarchical বা network মডেলের। ডেটা থাকত গাছ বা পয়েন্টারের জালে, আর প্রোগ্রামকে জানতে হতো কোন রেকর্ড থেকে কোন পয়েন্টার ধরে কোথায় যেতে হবে। ডিস্কে ডেটার সাজানো বদলালে প্রোগ্রাম ভাঙত। প্রতিটি নতুন প্রশ্নের জন্য নতুন কোড লিখতে হতো।

Codd-এর প্রস্তাব। IBM-এর গবেষক, গণিতে প্রশিক্ষিত Edgar F. Codd ১৯৭০ সালে Communications of the ACM পত্রিকায় “A Relational Model of Data for Large Shared Data Banks” প্রকাশ করেন। মূল কথা: ডেটা থাকবে গণিতের relation-এ — অর্থাৎ সারি-কলামের টেবিলে — আর প্রশ্ন করা হবে set theory আর predicate logic-এর ভিত্তিতে। প্রোগ্রাম বলবে কী চাই, ডিস্কে কীভাবে আছে তা জানবে না। একে বলে data independence. মজার ব্যাপার, IBM শুরুতে উৎসাহী ছিল না, কারণ IMS থেকে তাদের ভালো আয় হচ্ছিল। এই কাজের জন্য Codd ১৯৮১ সালে Turing Award পান।

System R আর SEQUEL. ধারণাটি বাস্তবে কাজ করে কি না দেখতে IBM-এর San Jose গবেষণাগারে ১৯৭০-এর দশকের মাঝামাঝি System R প্রজেক্ট শুরু হয়। সেখানে Donald Chamberlin আর Raymond Boyce একটি ইংরেজির মতো পড়া যায় এমন প্রশ্নের ভাষা বানান — SEQUEL (Structured English Query Language). নামটি অন্য একটি কোম্পানির trademark-এর সাথে মিলে যাওয়ায় পরে হয় SQL — যে কারণে আজও অনেকে “সিকুয়েল” উচ্চারণ করেন। System R প্রমাণ করে যে declarative প্রশ্ন থেকে দক্ষ execution plan বানানো সম্ভব — আজকের query optimizer-এর শিকড় সেখানেই।

বাণিজ্যে। IBM-এর প্রকাশিত গবেষণা পড়ে Larry Ellison-এর ছোট কোম্পানি ১৯৭৯ সালে বাজারে আনে Oracle — প্রথম বাণিজ্যিক SQL ডেটাবেসগুলোর একটি। ১৯৮৬ সালে ANSI আর পরের বছর ISO SQL-কে মানদণ্ড করে। তারপর পঞ্চাশ বছরে অসংখ্য নতুন ডেটাবেস এসেছে, কিন্তু SQL টিকে আছে — এমনকি অনেক NoSQL ডেটাবেসও পরে SQL-এর মতো ভাষা যোগ করেছে।

মূল উপলব্ধি: relational মডেলের আসল আবিষ্কার টেবিল নয় — “কী চাই” আর “কীভাবে আনব”-কে আলাদা করা। JDBC-তে আপনি যখন SQL পাঠান, এই বিভাজনেরই সুবিধা নেন: ডেটাবেস নিজের index আর plan দিয়ে কাজটা করে।

প্র ০২JDBC কেন একটি interface-এর সমষ্টি, কোনো লাইব্রেরি নয়? ODBC থেকে কী নেওয়া, আর এই নকশার দাম কী?

সমস্যা। ১৯৮০-এর দশকে প্রতিটি ডেটাবেস কোম্পানির নিজস্ব C লাইব্রেরি ছিল। Oracle-এর জন্য লেখা প্রোগ্রাম Sybase-এ চালাতে হলে ডেটাবেস-অংশ নতুন করে লিখতে হতো। এর সমাধানে SQL Access Group একটি সাধারণ “Call Level Interface” নকশা করে, আর তার ভিত্তিতে Microsoft ১৯৯২ সালে আনে ODBC (Open Database Connectivity): অ্যাপ একটি মানসম্মত API ডাকে, প্রতিটি ডেটাবেসের জন্য আলাদা driver অনুবাদ করে।

JDBC-র জন্ম। Java তখন নতুন, আর Sun চাইছিল ব্যবসায়িক অ্যাপে Java ব্যবহার হোক — যার মানে ডেটাবেস। তারা ODBC-র একই চিন্তা নিল, কিন্তু Java-র object-oriented রূপে: Connection, Statement, ResultSet হলো interface, আর driver সেগুলো implement করবে। JDBC ১৯৯৭ সালে JDK 1.1-এর অংশ হয়। শুরুতে অনেক ডেটাবেসের Java driver ছিল না, তাই Sun একটি JDBC-ODBC bridge দিয়েছিল — Java থেকে ODBC driver ডাকার সেতু। Java driver সহজলভ্য হওয়ার পর সেটি আর দরকার পড়েনি; Java 8-এ তা সরিয়ে ফেলা হয়।

নকশার সুবিধা। এটি পর্ব ২১-এর “interface-এর বিপরীতে প্রোগ্রাম করুন” নীতির একটি নিখুঁত উদাহরণ। আপনার কোড শুধু java.sql চেনে; driver JAR বদলালে, URL বদলালে PostgreSQL থেকে MySQL. JDBC 4.0 (Java 6) থেকে driver নিজেকে service loader দিয়ে নিবন্ধিত করে, তাই classpath-এ থাকলেই হয়। আর পুরো Java ecosystem — Hibernate, Spring, jOOQ, Flyway — এই এক interface-এর ওপর দাঁড়িয়ে, তাই যেকোনো নতুন ডেটাবেস শুধু একটি driver লিখেই সবগুলো টুল পেয়ে যায়।

দাম। interface একই হলেও SQL একই নয়। id বানানো (IDENTITY বনাম AUTO_INCREMENT), পেজিং (LIMIT বনাম FETCH FIRST), তারিখের ফাংশন — ডেটাবেসভেদে আলাদা। তাই “শুধু driver বদলালেই হবে” কথাটি পুরো সত্য নয়। আর সর্বনিম্ন সাধারণ রূপ হওয়ায় JDBC বেশ শব্দবহুল — এজন্যই এর ওপরে এত টুল।

মূল উপলব্ধি: একটি ভালো interface ২৫ বছরেরও বেশি টিকে থাকতে পারে, কারণ সে শুধু চুক্তি দেয়, implementation নয়। আপনার Repository interface-ও একই কাজ করে — ছোট স্কেলে।

প্র ০৩ACID-এর প্রতিশ্রুতি এত মূল্যবান হলে NoSQL ডেটাবেসগুলো কেন অনেক সময় তা ছেড়ে দেয়? BASE কী?

ACID-এর ইতিহাস। transaction-এর ধারণা গড়ে তোলায় সবচেয়ে বড় ভূমিকা Jim Gray-র, যিনি ১৯৭০-এর দশকে IBM-এ System R-এ কাজ করেছেন আর ১৯৮১ সালে “The Transaction Concept: Virtues and Limitations” লেখেন; ১৯৯৮ সালে তিনি Turing Award পান। “ACID” নামটি দেন জার্মান গবেষক Theo Härder আর Andreas Reuter, ১৯৮৩ সালের “Principles of Transaction-Oriented Database Recovery” প্রবন্ধে — চারটি বৈশিষ্ট্যের আদ্যক্ষর।

ACID-এর দাম। একটি server-এ ACID তুলনামূলক সহজ: lock, log, আর একটি ডিস্ক। কিন্তু ২০০০-এর দশকে Google, Amazon-এর মতো কোম্পানির ডেটা এক মেশিনে আঁটে না — হাজারো মেশিনে, কয়েকটি দেশে ছড়ানো। তখন প্রতিটি লেখার জন্য সব কপিকে একমত করাতে নেটওয়ার্কে বারবার যাওয়া-আসা লাগে, আর একটি অংশ বিচ্ছিন্ন হলে পুরো ব্যবস্থা থেমে যেতে পারে। Eric Brewer ২০০০ সালে CAP অনুমান দেন: নেটওয়ার্ক বিভক্ত হলে consistency আর availability-র মধ্যে একটিকে বেছে নিতে হয়; ২০০২ সালে Seth Gilbert আর Nancy Lynch এর একটি রূপ গাণিতিকভাবে প্রমাণ করেন।

BASE. ACID-এর বিপরীতে মজা করে বানানো শব্দ — Basically Available, Soft state, Eventually consistent. ধারণাটি Brewer-এর গবেষণা দলের ১৯৯০-এর দশকের শেষের কাজ থেকে আসে, আর ২০০৮ সালে eBay-এর Dan Pritchett-এর “BASE: An Acid Alternative” প্রবন্ধে জনপ্রিয় হয়। মূল কথা: সবসময় সাড়া দাও, আর কপিগুলো কিছুক্ষণ পর একমত হবে — eventual consistency. সোশ্যাল মিডিয়ায় একটি “like” কয়েক সেকেন্ড দেরিতে সবার কাছে পৌঁছালে কারও ক্ষতি নেই; Amazon-এর Dynamo, Cassandra-র মতো ব্যবস্থা এই পথে গেছে।

পেন্ডুলাম ফিরে আসা। কিন্তু টাকা, স্টক, টিকিটে “কিছুক্ষণ পর ঠিক হবে” চলে না — একই আসন দুজনকে বিক্রি হয়ে যায়। তাই অনেক NoSQL ডেটাবেস পরে transaction যোগ করেছে (MongoDB ৪.০ থেকে একাধিক document-এ), আর Google Spanner-এর মতো ব্যবস্থা বিশ্বজুড়ে ছড়িয়েও শক্ত consistency দেয়। বাংলাদেশের প্রেক্ষাপটে ভাবুন: মোবাইল ব্যাংকিংয়ের লেনদেন ACID চায়; অ্যাপের নোটিফিকেশন গোনা BASE-এ চলে।

মূল উপলব্ধি: ACID বনাম BASE কোনো ধর্মযুদ্ধ নয়, একটি প্রকৌশলের লেনদেন। প্রশ্ন করুন: এই ডেটায় কিছুক্ষণের অসামঞ্জস্য হলে ক্ষতি কত? টাকার ক্ষেত্রে উত্তর প্রায় সবসময় — অনেক।

প্র ০৪object আর টেবিলের অমিল — “object-relational impedance mismatch” কী, আর কেউ কেউ ORM-কে “কম্পিউটার বিজ্ঞানের ভিয়েতনাম” বলেছিলেন কেন?

অমিলটা কোথায়। Java-তে ডেটা থাকে object-এর গ্রাফে: একটি Customer-এর ভেতরে List<Order>, প্রতিটি Order-এর ভেতরে প্রোডাক্ট, inheritance, reference দিয়ে পরিচয়। relational ডেটাবেসে আছে সমতল টেবিল, foreign key, আর primary key দিয়ে পরিচয়। দুটি ভিন্ন জগৎ: Java-তে দুটি object একই কি না তা == বা equals দিয়ে, ডেটাবেসে id দিয়ে; Java-তে inheritance আছে, টেবিলে নেই; Java-তে গ্রাফ ধরে হাঁটা যায়, ডেটাবেসে প্রতিটি পদক্ষেপ একটি query বা JOIN. বৈদ্যুতিক প্রকৌশলের শব্দ ধার করে এই অমিলকে বলে impedance mismatch.

ORM-এর প্রতিশ্রুতি। Object-Relational Mapping টুল — Java-তে ২০০১ সালে Gavin King-এর শুরু করা Hibernate, পরে ২০০৬ সালে মানদণ্ড হিসেবে JPA — এই অনুবাদ নিজে করতে চায়। class-এ annotation দিন, save(customer) ডাকুন, SQL ORM লিখবে। সাধারণ CRUD-এ এটি দারুণ সময় বাঁচায়।

Neward-এর প্রবন্ধ। ২০০৬ সালে Ted Neward “The Vietnam of Computer Science” নামে একটি দীর্ঘ ব্লগ-প্রবন্ধ লেখেন। তাঁর তুলনার সারকথা (আমাদের ভাষায়): ORM প্রজেক্ট শুরুতে দ্রুত সাফল্য দেয়, তারপর একের পর এক কঠিন কোণ — inheritance কীভাবে টেবিলে রাখব, lazy loading কখন query চালাবে, cache আর ডেটাবেসের মধ্যে কোনটা সত্য — আর প্রতিটি সমাধানে আরও বেশি জটিলতা ও বিনিয়োগ লাগে, অথচ বের হওয়ার স্পষ্ট পথ থাকে না। তাঁর মতে মূল সমস্যা প্রযুক্তিগত নয়, ধারণাগত — দুটি মডেলকে পুরোপুরি মেলানো সম্ভব নয়, তাই কোথাও একটি আপস করতেই হয়।

পরে কী হলো। ORM বিলুপ্ত হয়নি — JPA আজও Java-র সবচেয়ে প্রচলিত ডেটা-স্তর। কিন্তু অভিজ্ঞ দলগুলো আপসটা সচেতনভাবে করে: সাধারণ CRUD-এ ORM, জটিল রিপোর্ট আর বড় batch-এ সরাসরি SQL (JdbcClient বা jOOQ), আর record-এর মতো সরল ডেটা-বাহক দিয়ে ফল ফেরত। এই পর্বের Repository pattern সেই আপসেরই রূপ — interface-এর পেছনে যা খুশি থাকুক।

মূল উপলব্ধি: যে টুল একটি গভীর অমিল “লুকিয়ে” দেয়, সে অমিলটা মুছে দেয় না — শুধু সরিয়ে রাখে, যতক্ষণ না কোনো N+1 বা অদ্ভুত বাগে তা আবার দেখা দেয়। তাই ORM ব্যবহারের আগে SQL আর JDBC বোঝা জরুরি।

প্র ০৫SQL injection-এর সমাধান বহু বছর ধরে জানা। তবু OWASP Top 10 তালিকায় injection এত দীর্ঘ সময় ওপরের দিকে থাকল কেন?

OWASP আর তালিকা। Open Worldwide Application Security Project (আগে Open Web Application Security Project) একটি অলাভজনক সংগঠন, যারা ওয়েব অ্যাপের সবচেয়ে গুরুতর ঝুঁকিগুলোর তালিকা প্রকাশ করে — OWASP Top 10, প্রথমবার ২০০৩ সালে। Injection (SQL injection যার সবচেয়ে পরিচিত রূপ) ২০০৭ সংস্করণে দ্বিতীয়, ২০১০, ২০১৩ আর ২০১৭-তে প্রথম, আর ২০২১-এ তৃতীয় অবস্থানে ছিল। অর্থাৎ প্রায় পনেরো বছর প্রথম তিনের মধ্যে।

পুরোনো সমস্যা। এই দুর্বলতা নিয়ে প্রকাশ্য লেখার সবচেয়ে পরিচিত প্রথম দিকের উদাহরণ ১৯৯৮ সালে Phrack পত্রিকায় Jeff Forristal-এর লেখা। আর parameterized query — এই পর্বের ? — তারও আগে থেকে ডেটাবেস API-তে আছে। অর্থাৎ প্রতিকার জানা, সহজ, আর বিনা খরচের।

তবু কেন টিকে আছে। কয়েকটি কারণ একসাথে কাজ করে। প্রথমত, ভুল পথটি সবচেয়ে স্বাভাবিক মনে হয় — String জোড়া লাগানো প্রত্যেক নতুন প্রোগ্রামার প্রথম সপ্তাহেই শেখেন। দ্বিতীয়ত, ভুল কোড সাধারণ ইনপুটে নিখুঁত চলে; কোনো টেস্ট বা demo-তে ধরা পড়ে না। তৃতীয়ত, পুরোনো কোডের পাহাড় — দশ-বিশ বছর আগের লেখা সিস্টেম আজও চলছে। চতুর্থত, injection শুধু SQL-এ নয়: OS command, LDAP, টেমপ্লেট, এমনকি আজকের LLM-এর prompt injection — যেখানেই ডেটা আর নির্দেশ এক চ্যানেলে মেশে। আর নতুন কারণ: AI-এর লেখা কোড পুরোনো টিউটোরিয়ালের অভ্যাস নতুন করে ছড়ায়।

কী কাজ করেছে। র‍্যাংক নামার পেছনে প্রধান ভূমিকা টুলের: ORM আর JdbcClient-এর মতো লাইব্রেরি ডিফল্টে parameter ব্যবহার করে, static analysis টুল জোড়া লাগানো SQL ধরে, আর framework নিরাপদ পথকেই সহজ পথ বানিয়েছে। নিরাপত্তার ইতিহাসের একটি পুনরাবৃত্ত শিক্ষা — মানুষকে সতর্ক থাকতে বলার চেয়ে নিরাপদ পথটিকে ডিফল্ট করা বেশি কার্যকর।

মূল উপলব্ধি: injection-এর মূল কারণ একটাই — কোড আর ডেটা একই চ্যানেলে। যেকোনো নতুন প্রযুক্তিতে প্রথম প্রশ্ন করুন: বাইরের ইনপুট কি কোনোভাবে নির্দেশ হিসেবে পড়া হতে পারে?

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. একটি নিরীহ নাম: অংশ ৫-এর প্রোগ্রামে ইনপুটের তালিকায় "D'Costa" যোগ করুন (method-গুলোকে ফোনের বদলে নাম ভাবুন)। না চালিয়ে বলুন জোড়া SQL কেমন দেখাবে আর ডেটাবেস তা পেলে কী করত। নিরাপদ পথে কী হতো?

    জোড়া SQL: ... WHERE phone = 'D'Costa'. ডেটাবেস 'D'-কে পুরো লেখা হিসেবে পড়ত, তারপর Costa' অর্থহীন — SQL syntax error, আর ব্যবহারকারী একটি ভাঙা পেজ দেখতেন। কোনো আক্রমণ ছাড়াই, শুধু একটি সাধারণ বাংলাদেশি পদবির কারণে। নিরাপদ পথে SQL একই থাকে আর params : [D'Costa] — ডেটাবেস নামটি হুবহু খুঁজত। শিক্ষা: PreparedStatement শুধু নিরাপত্তা নয়, সঠিকতাও।

  2. Repository-তে নতুন method: অংশ ৮-এর প্রোগ্রামে interface-এ List<Customer> findByDistrict(String district) যোগ করে Map-সংস্করণে implement করুন, আর main-এ ঢাকার ক্রেতাদের ছাপুন। JDBC-সংস্করণে এর SQL কী হবে?
    // interface-এ
    List<Customer> findByDistrict(String district);
    
    // InMemoryCustomerRepository-তে
    public List<Customer> findByDistrict(String district) {
        return byPhone.values().stream().filter(c -> c.district().equals(district)).toList();
    }
    
    // main-এ
    System.out.println(repo.findByDistrict("ঢাকা"));

    একমাত্র ঢাকার ক্রেতা রহিম, তাই আউটপুট [Customer[id=1, name=রহিম, phone=01711000001, district=ঢাকা]] (রহিমা বাতিল হয়েছিলেন)। LinkedHashMap-এর কারণে ক্রম ঢোকানোর ক্রম অনুযায়ী, যা এখানে id-র ক্রমও। JDBC-সংস্করণে SQL: SELECT id, name, phone, district FROM customers WHERE district = ? ORDER BY id, আর while (rs.next()) দিয়ে তালিকায় যোগ। দ্রুত খোঁজার জন্য district কলামে একটি index যোগ করা যায়।

  3. কোথায় transaction? অনলাইন দোকানে একটি অর্ডার দেওয়া মানে: (ক) orders-এ একটি সারি, (খ) order_items-এ তিনটি সারি, (গ) প্রতিটি প্রোডাক্টের স্টক কমানো, (ঘ) ক্রেতাকে SMS পাঠানো। কোনগুলো এক transaction-এ যাবে, আর কোনটা নয় কেন?

    (ক), (খ), (গ) — এক transaction-এ। আইটেম ছাড়া অর্ডার বা অর্ডার ছাড়া স্টক কমা দুটিই ভুল অবস্থা; একটি ব্যর্থ হলে সবগুলো rollback. স্টক কমানোর UPDATE-এ শর্ত রাখুন (WHERE id = ? AND stock >= ?) আর ফেরত-সংখ্যা যাচাই করুন — ০ হলে স্টক শেষ, rollback. (ঘ) SMS বাইরে, commit-এর পরে: ডেটাবেস SMS ফেরত নিতে পারে না, তাই transaction-এর ভেতরে পাঠালে rollback হলেও ক্রেতা “অর্ডার সফল” বার্তা পেয়ে যেতেন। ধীর বাইরের কাজ transaction-এর ভেতরে রাখলে lock-ও অনেকক্ষণ ধরে থাকে।

  4. query গুনুন: একটি পেজে ৫০টি অর্ডার দেখায়, প্রতিটির পাশে ক্রেতার নাম আর প্রতিটি অর্ডারের আইটেমের সংখ্যা। লুপের ভেতরে প্রতিটির জন্য আলাদা query চালালে মোট কয়টি query? কীভাবে একটি বা দুটিতে নামাবেন?

    অর্ডারের তালিকায় ১টি, প্রতিটি অর্ডারের ক্রেতার জন্য ৫০টি, আইটেম গোনার জন্য আরও ৫০টি — মোট ১০১টি। এটি N+1 (এখানে আসলে 2N+1). একটি query-তে: orders-এর সাথে customers JOIN, আর order_items LEFT JOIN করে GROUP BY o.id, c.name সহ COUNT(i.id). ক্রেতার জন্য একটি JOIN আর আইটেমের জন্য আলাদা একটি GROUP BY query — দুটি query-ও পুরোপুরি গ্রহণযোগ্য। নিয়ম: query-র সংখ্যা ডেটার আকারের ওপর নির্ভর করবে না।

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

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

ক্রমJDBC দিয়ে একটি SELECT চালানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🔌 DataSource থেকে Connection নেওয়া
  2. 📝 ? সহ SQL দিয়ে PreparedStatement তৈরি
  3. 📌 setString / setLong দিয়ে মান বসানো
  4. 🚀 executeQuery() দিয়ে ResultSet পাওয়া
  5. 📋 rs.next() দিয়ে সারি পড়ে record বানানো
  6. 🔒 ResultSet, PreparedStatement, Connection উল্টো ক্রমে বন্ধ

Java JDBC-তে PreparedStatement-এর ps.setString(i, value)-এ প্রথম ? চিহ্নের index কত?

  • 0
  • 1
  • -1
  • যেকোনো সংখ্যা, driver নিজে মিলিয়ে নেয়

JDBC-তে UPDATE wallets SET balance = 0 WHERE id = ? চালাতে ps.executeUpdate() কী ফেরত দেয়?

  • একটি ResultSet
  • সফল হলে true
  • প্রভাবিত সারির সংখ্যা (int)
  • কিছুই না (void)

একটি নতুন JDBC Connection-এ setAutoCommit কিছু না ডাকলে কী অবস্থা থাকে?

  • auto-commit চালু — প্রতিটি statement চলামাত্র নিজেই commit হয়
  • কোনো বদল commit() না ডাকা পর্যন্ত স্থায়ী হয় না
  • শুধু SELECT auto-commit হয়, UPDATE হয় না
  • ডিফল্ট নেই, প্রতিবার ঠিক করতে হয়

Java অ্যাপে ব্যবহারকারীর লেখা ফোন নম্বর দিয়ে খোঁজার সময় SQL injection ঠেকানোর সঠিক উপায় কোনটি?

  • ইনপুটের ' চিহ্ন হাতে replace করে '' বানানো
  • Statement ব্যবহার করে ইনপুট trim() করা
  • ডেটাবেসের error বার্তা ব্যবহারকারীকে না দেখানো
  • PreparedStatement-এ ? রেখে setString দিয়ে মান বসানো

ডেটাবেস transaction-এর ACID-এর A (Atomicity) বলতে কী বোঝায়?

  • commit হওয়ার পর crash হলেও ডেটা হারায় না
  • ভেতরের সব বদল একসাথে হয়, নয়তো একটিও না
  • একসাথে চলা transaction একে অপরের অর্ধেক কাজ দেখে না
  • সব নিয়ম ও constraint মেনে চলা

৫০টি অর্ডার একটি query-তে এনে, তারপর লুপে প্রতিটি অর্ডারের ক্রেতাকে আলাদা query দিয়ে আনলে ডেটাবেসে মোট কয়টি query যায়?

  • ১টি
  • ৫০টি
  • ৫১টি
  • ১০০টি

শব্দকোষ

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

মনে রাখুন

  1. ডেটাবেস দেয় concurrency, দ্রুত query আর transaction: relational মডেলে টেবিল, primary key আর foreign key; ছাঁকা, জোড়া (JOIN) আর গোনার (GROUP BY) কাজ ডেটাবেসেই করান।
  2. JDBC = interface, driver = implementation: DataSource → Connection → PreparedStatement → ResultSet → record. driver না থাকলে No suitable driver found; parameter আর কলামের index শুরু ১ থেকে।
  3. ইনপুট কখনো SQL-এর লেখায় নয়: সবসময় ? আর setXxx; টেবিল/কলামের নাম বাইরে থেকে এলে allow-list.
  4. একসাথে হতে হবে এমন বদল এক transaction-এ: setAutoCommit(false), commit, catch-এ rollback; executeUpdate()-এর সংখ্যা যাচাই; ACID মনে রাখুন।
  5. পেশাদার কাঠামো: try-with-resources, HikariCP pool, interface-এর পেছনে Repository, লুপে query নয় (N+1), migration-এ schema, credential environment variable-এ আর কম-অনুমতির ব্যবহারকারী।

পরের পর্বে: Spring Boot-এর শুরু — এই পর্বে হাতে যে DataSource, pool আর Repository জুড়েছেন, Spring Boot কীভাবে সেগুলো নিজে বানিয়ে আপনার class-এ পৌঁছে দেয়, আর কেন বড় Java প্রজেক্টের বেশিরভাগই আজ Spring-এর ওপর দাঁড়িয়ে।

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

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