Skip to main content
Innovation|Innovation

Spring Data JPA का रहस्योद्घाटन: Tables से Java Objects तक

Spring Data JPA का एक व्यापक guide जो ORM अवधारणाओं, entity mappings, relationships, N+1 समस्या और इसके समाधान, derived queries, JPQL, native SQL, transaction isolation और propagation, Flyway migrations, और database indexing strategies को कवर करता है।

11 अप्रैल 202618 min read

JPA क्यों मौजूद है: अनुवाद की समस्या

कल्पना कीजिए कि आप English बोलते हैं और आपका दोस्त Spanish बोलता है। आपको बीच में एक translator की आवश्यकता है ताकि आप एक दूसरे को समझ सकें। यह ठीक वही है जो JPA करता है — यह आपकी Java objects और आपके database tables के बीच अनुवाद करता है।

आपका database rows और columns में सोचता है। आपका Java code objects और fields में सोचता है। JPA के बिना, आप raw SQL लिखेंगे, result sets से data को manually खींचेंगे, और इसे objects में भरेंगे — हर एक query के लिए बार-बार। JPA उस पूरे अनुवाद को automate करता है।

ORM क्या है? (जैसे आप 10 साल के हैं)

एक library के बारे में सोचें। Library में cards के साथ एक catalog system है (यह आपका database है)। प्रत्येक card में जानकारी है: book title, author, shelf number। अब कल्पना कीजिए कि आप अपने treehouse में books के साथ काम करना चाहते हैं। आप catalog cards नहीं ले जाना चाहते — आप वास्तविक book objects चाहते हैं जिन्हें आप पकड़ सकें, पढ़ सकें, और पास कर सकें।

एक ORM (Object-Relational Mapping) वह जादुई librarian है जो catalog cards लेता है और उन्हें आपके लिए वास्तविक book objects में बदल देता है। जब आप एक book object को बदलते हैं (मान लीजिए, आप इसमें एक note लिखते हैं), librarian वापस जाता है और catalog card को भी update करता है।

  • JPA — specification (नियम पुस्तक कि जादुई librarian को कैसे काम करना चाहिए)
  • Hibernate — implementation (वास्तविक librarian जो उन नियमों का पालन करता है)
  • Spring Data JPA — convenience layer (आप librarian को विस्तृत निर्देशों के बजाय plain English में बताते हैं कि आप क्या चाहते हैं)

@Entity: एक Table को एक Java Class में बदलना

@Entity annotation JPA को बताता है: "यह Java class एक database table से map होती है।" Class का प्रत्येक instance एक row का प्रतिनिधित्व करता है। प्रत्येक field एक column का प्रतिनिधित्व करता है।

@Entity
@Table(name = "books")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 200)
    private String title;

    @Column(name = "page_count")
    private int pageCount;

    @Column(nullable = false)
    private String isbn;

    @Column(name = "published_date")
    private LocalDate publishedDate;

    // Getters and setters omitted for brevity
}

आइए annotations को break down करें:

  • @Id — primary key को mark करता है। हर entity के पास एक होनी चाहिए।
  • @GeneratedValue(strategy = GenerationType.IDENTITY) — database स्वचालित रूप से ID generate करता है (एक auto-increment column की तरह)।
  • @Column — column को fine-tune करता है: name, nullability, length। यदि आप इसे छोड़ देते हैं, तो JPA field name को column name के रूप में उपयोग करता है।
  • @Table(name = "books") — स्पष्ट रूप से table name set करता है। इसके बिना, JPA class name का उपयोग करता है।

Relationships: Tables कैसे जुड़ते हैं

वास्तविक जीवन में, चीजें जुड़ी हुई हैं। एक author कई books लिखता है। एक student कई courses में enroll होता है। एक order में कई items होते हैं। JPA आपको इन connections को सीधे अपनी Java classes में express करने देता है।

@OneToMany और @ManyToOne — Parent-Child Connection

एक classroom के बारे में सोचें। एक teacher के कई students हैं। Teacher की तरफ से, यह "one to many" है। प्रत्येक student की तरफ से, यह "many to one" है — कई students एक teacher साझा करते हैं।

@Entity
@Table(name = "authors")
public class Author {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Book> books = new ArrayList<>();

    // Helper method to keep both sides in sync
    public void addBook(Book book) {
        books.add(book);
        book.setAuthor(this);
    }
}

@Entity
@Table(name = "books")
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id", nullable = false)
    private Author author;
}

मुख्य बिंदु:

  • mappedBy = "author" — JPA को बताता है कि Book entity relationship का owner है (इसमें foreign key column author_id है)।
  • cascade = CascadeType.ALL — जब आप एक author को save/delete करते हैं, तो उनकी सभी books भी save/delete हो जाती हैं।
  • orphanRemoval = true — यदि आप author की list से एक book हटाते हैं, तो JPA इसे database से delete कर देता है।
  • fetch = FetchType.LAZY — book load करते समय author को load न करें जब तक कि आप वास्तव में इसे access न करें। यह performance के लिए महत्वपूर्ण है।

@ManyToMany — Network Connection

Students और courses एक perfect उदाहरण हैं। एक student कई courses लेता है। एक course के कई students हैं। कोई भी दूसरे का "owner" नहीं है — वे एक join table साझा करते हैं।

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(
        name = "student_courses",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private Set<Course> courses = new HashSet<>();
}

@Entity
@Table(name = "courses")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students = new HashSet<>();
}

@JoinTable bridge table को define करता है। JPA दो foreign key columns के साथ student_courses बनाता है। Hibernate की bag semantics के साथ duplicate-row bug से बचने के लिए ManyToMany के लिए List के बजाय Set का उपयोग करें।

N+1 समस्या: हज़ारों Queries से मौत

यह JPA में सबसे आम performance trap है। आइए फिर से librarian उपमा का उपयोग करें।

कल्पना कीजिए आप librarian से पूछते हैं: "मुझे सभी 50 authors दें।" Librarian आपके पास 50 author cards लाता है। फिर आप कहते हैं: "अब मुझे प्रत्येक author के लिए books दिखाएँ।" Librarian 50 अलग बार shelf पर चलता है — प्रत्येक author के लिए एक बार। यह authors के लिए 1 query + books के लिए 50 queries = 51 queries है। यह N+1 समस्या है।

Smart approach? Librarian को एक list दें और कहें: "मुझे एक trip में सभी authors और उनकी books लाएँ।" यही नीचे दिए गए solutions करते हैं।

इसे कैसे पहचानें

application.yml में SQL logging enable करें:

spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE

यदि आप विभिन्न IDs के साथ एक ही SELECT को दोहराते हुए देखते हैं, तो आपके पास एक N+1 समस्या है।

Solution 1: JOIN FETCH (JPQL)

JPA को एक JOIN का उपयोग करके एक single query में सब कुछ load करने के लिए कहें।

public interface AuthorRepository extends JpaRepository<Author, Long> {

    @Query("SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = :id")
    Optional<Author> findByIdWithBooks(@Param("id") Long id);

    @Query("SELECT DISTINCT a FROM Author a JOIN FETCH a.books")
    List<Author> findAllWithBooks();
}

JOIN FETCH Hibernate को बताता है: "जब आप authors को load करते हैं, तो उनकी books को भी उसी SQL query में load करें।" DISTINCT keyword result में duplicate authors को रोकता है जब प्रति author कई books मौजूद होती हैं।

Solution 2: @EntityGraph (Declarative)

यदि आप JPQL नहीं लिखना चाहते, तो eagerly क्या fetch करना है यह declaratively specify करने के लिए @EntityGraph का उपयोग करें।

public interface AuthorRepository extends JpaRepository<Author, Long> {

    @EntityGraph(attributePaths = {"books"})
    List<Author> findAll();

    @EntityGraph(attributePaths = {"books"})
    Optional<Author> findById(Long id);
}

@EntityGraph उस विशिष्ट query के लिए default LAZY fetch को override करता है। यह पर्दे के पीछे एक LEFT JOIN generate करता है। JOIN FETCH पर लाभ यह है कि आपको कोई JPQL लिखने की आवश्यकता नहीं है।

Solution 3: DTO Projections (Read-Only के लिए सर्वश्रेष्ठ)

कभी-कभी आपको पूरी entity की आवश्यकता नहीं होती — बस विशिष्ट fields की। DTO projections केवल वह fetch करते हैं जो आपको चाहिए, managed entities के overhead को पूरी तरह से skip करते हैं।

// DTO class
public record AuthorSummary(String name, long bookCount) {}

// Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {

    @Query("SELECT new com.example.dto.AuthorSummary(a.name, COUNT(b)) " +
           "FROM Author a LEFT JOIN a.books b GROUP BY a.name")
    List<AuthorSummary> findAuthorSummaries();
}

DTO projections सबसे तेज़ विकल्प हैं क्योंकि:

  • वे Hibernate के entity tracking (persistence context) को skip करते हैं।
  • वे केवल वे columns fetch करते हैं जो आप specify करते हैं।
  • वे dashboards, reports, और list views के लिए perfect हैं जहाँ आप data display करते हैं लेकिन edit नहीं करते।

Derived Queries: Spring आपके Method Name को पढ़ता है

Spring Data JPA आपके method name से SQL generate कर सकता है। आप method signature लिखते हैं, Spring query लिखता है। यह जादू जैसा लगता है।

public interface BookRepository extends JpaRepository<Book, Long> {

    // SELECT * FROM books WHERE title = ?
    List<Book> findByTitle(String title);

    // SELECT * FROM books WHERE page_count > ?
    List<Book> findByPageCountGreaterThan(int pages);

    // SELECT * FROM books WHERE title LIKE '%keyword%' AND page_count < ?
    List<Book> findByTitleContainingAndPageCountLessThan(String keyword, int maxPages);

    // SELECT * FROM books WHERE author_id = ? ORDER BY published_date DESC
    List<Book> findByAuthorIdOrderByPublishedDateDesc(Long authorId);

    // SELECT COUNT(*) FROM books WHERE author_id = ?
    long countByAuthorId(Long authorId);

    // SELECT * FROM books WHERE isbn = ?
    Optional<Book> findByIsbn(String isbn);

    // DELETE FROM books WHERE page_count < ?
    void deleteByPageCountLessThan(int pages);
}

Spring method name को भागों में parse करता है: findBy + Title + Containing + And + PageCount + LessThan। प्रत्येक भाग एक SQL clause से map होता है। समर्थित keywords में शामिल हैं: And, Or, Between, LessThan, GreaterThan, Like, Containing, In, OrderBy, Not, IsNull, और अधिक।

@Query: जब Method Names बहुत लंबे हो जाते हैं

Derived queries सरल lookups के लिए बहुत अच्छी हैं। लेकिन जब queries जटिल हो जाती हैं, तो method names unreadable हो जाते हैं। तब आप @Query का उपयोग करते हैं।

JPQL (Java Persistence Query Language)

JPQL SQL की तरह दिखती है लेकिन tables और columns के बजाय entities और fields पर operate करती है।

public interface BookRepository extends JpaRepository<Book, Long> {

    // JPQL — uses entity names, not table names
    @Query("SELECT b FROM Book b WHERE b.author.name = :authorName AND b.pageCount > :minPages")
    List<Book> findByAuthorAndMinPages(
        @Param("authorName") String authorName,
        @Param("minPages") int minPages
    );

    // Update query
    @Modifying
    @Query("UPDATE Book b SET b.title = :newTitle WHERE b.id = :bookId")
    int updateTitle(@Param("bookId") Long bookId, @Param("newTitle") String newTitle);
}

Native SQL

जब आपको database-specific features (window functions, CTEs, specific index hints) की आवश्यकता होती है, तो native queries का उपयोग करें।

public interface BookRepository extends JpaRepository<Book, Long> {

    // Native SQL — uses actual table/column names
    @Query(value = "SELECT b.* FROM books b " +
                   "JOIN authors a ON b.author_id = a.id " +
                   "WHERE a.name = :authorName " +
                   "ORDER BY b.published_date DESC LIMIT 5",
           nativeQuery = true)
    List<Book> findLatestByAuthor(@Param("authorName") String authorName);

    // Window function — not possible in JPQL
    @Query(value = "SELECT b.title, b.page_count, " +
                   "RANK() OVER (ORDER BY b.page_count DESC) as rank " +
                   "FROM books b",
           nativeQuery = true)
    List<Object[]> findBooksRankedByLength();
}

कब किसका उपयोग करें:

  • Derived queries — 1-2 conditions के साथ सरल lookups
  • JPQL — मध्यम जटिलता, database स्वतंत्रता चाहते हैं
  • Native SQL — database-specific features, जटिल analytics, performance-critical queries

Transactions: सब कुछ या कुछ नहीं

एक transaction एक trip के लिए suitcase pack करने जैसा है। या तो सब कुछ अंदर जाता है (commit), या आप सब कुछ unpack करते हैं और फिर से शुरू करते हैं (rollback)। आप कभी भी आधे कपड़े pack करके airport पर नहीं पहुँचना चाहते।

एक banking transfer में: account A से deduct करें और account B में जोड़ें। यदि दूसरा step विफल होता है, तो पहले को undo किया जाना चाहिए। Transactions इसकी गारंटी देते हैं।

Transaction Isolation Levels

Isolation levels नियंत्रित करते हैं कि transactions एक दूसरे के uncommitted काम को कितना "देख" सकते हैं। इसे exam students के बीच walls की तरह सोचें:

  • READ_UNCOMMITTED — कोई walls नहीं। आप अपने पड़ोसी के अधूरे answers देख सकते हैं (dirty reads)। लगभग कभी उपयोग नहीं किया जाता।
  • READ_COMMITTED — पतली walls। आप केवल वे answers देखते हैं जो आपके पड़ोसी ने submit किए हैं (committed)। यह PostgreSQL के लिए default है।
  • REPEATABLE_READ — मोटी walls। एक बार जब आप एक value पढ़ लेते हैं, तो यह आपके transaction के दौरान नहीं बदलेगी, भले ही आपका पड़ोसी इसे update करे।
  • SERIALIZABLE — soundproof rooms। Transactions ऐसे execute होते हैं जैसे वे एक समय में एक चल रहे हों। सबसे सुरक्षित लेकिन सबसे धीमा।
@Service
public class TransferService {

    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId)
            .orElseThrow(() -> new AccountNotFoundException(fromId));
        Account to = accountRepository.findById(toId)
            .orElseThrow(() -> new AccountNotFoundException(toId));

        if (from.getBalance().compareTo(amount) < 0) {
            throw new InsufficientFundsException(fromId, amount);
        }

        from.setBalance(from.getBalance().subtract(amount));
        to.setBalance(to.getBalance().add(amount));

        accountRepository.save(from);
        accountRepository.save(to);
        // If any line throws, EVERYTHING rolls back
    }
}

Transaction Propagation

Propagation नियंत्रित करता है कि क्या होता है जब एक transactional method दूसरे transactional method को call करता है। इसे Russian nesting dolls की तरह सोचें:

// REQUIRED (default) — join existing transaction or create new one
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
    orderRepository.save(order);
    paymentService.processPayment(order); // joins THIS transaction
    // If payment fails, order save is also rolled back
}

// REQUIRES_NEW — always create a brand new transaction
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuditEvent(String event) {
    auditRepository.save(new AuditLog(event));
    // Even if the calling transaction rolls back,
    // this audit log is saved — it has its own transaction
}

// SUPPORTS — use transaction if one exists, run without one if not
@Transactional(propagation = Propagation.SUPPORTS)
public Book findBook(Long id) {
    return bookRepository.findById(id).orElse(null);
}

// MANDATORY — must be called within an existing transaction
@Transactional(propagation = Propagation.MANDATORY)
public void deductInventory(Long productId, int quantity) {
    // Throws exception if no transaction exists
    // Forces callers to wrap this in their own transaction
}

// NEVER — must NOT be called within a transaction
@Transactional(propagation = Propagation.NEVER)
public Report generateReport() {
    // Guaranteed no transaction overhead for read-heavy operations
    return reportRepository.buildReport();
}

आम pitfall: एक ही class के भीतर एक @Transactional method को call करना एक नया transaction नहीं बनाता। Spring का proxy केवल class के बाहर से calls को intercept करता है। यदि आपको आंतरिक रूप से एक transactional method को call करना है, तो class को स्वयं में inject करें या method को एक अलग service में निकालें।

Flyway Migrations: आपके Database के लिए Version Control

आपका code Git में रहता है। आपका database schema भी वहीं रहना चाहिए। Flyway track करता है कि कौन से SQL scripts apply किए गए हैं और startup पर नए को स्वचालित रूप से चलाता है।

इसे एक checklist की तरह सोचें। Flyway प्रत्येक script को चलाते समय इसे mark off करता है। यदि आप दो नए scripts के साथ एक नया version deploy करते हैं, तो Flyway केवल उन दो को चलाता है — यह पुराने को कभी फिर से नहीं चलाता।

Naming Convention

src/main/resources/db/migration/
  V1__create_books_table.sql
  V2__create_authors_table.sql
  V3__add_author_id_to_books.sql
  V4__create_student_courses_table.sql
  V5__add_isbn_index.sql

Naming format: V{number}__{description}.sql (version number के बाद दो underscores)। Flyway उन्हें version order में चलाता है।

Example Migrations

-- V1__create_books_table.sql
CREATE TABLE books (
    id          BIGSERIAL PRIMARY KEY,
    title       VARCHAR(200) NOT NULL,
    isbn        VARCHAR(13)  NOT NULL UNIQUE,
    page_count  INT          NOT NULL DEFAULT 0,
    published_date DATE,
    created_at  TIMESTAMP    NOT NULL DEFAULT NOW()
);

-- V2__create_authors_table.sql
CREATE TABLE authors (
    id         BIGSERIAL PRIMARY KEY,
    name       VARCHAR(100) NOT NULL,
    email      VARCHAR(255) UNIQUE,
    created_at TIMESTAMP    NOT NULL DEFAULT NOW()
);

-- V3__add_author_id_to_books.sql
ALTER TABLE books ADD COLUMN author_id BIGINT;
ALTER TABLE books ADD CONSTRAINT fk_books_author
    FOREIGN KEY (author_id) REFERENCES authors(id);

-- V4__create_student_courses_table.sql
CREATE TABLE students (
    id   BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE courses (
    id    BIGSERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL
);

CREATE TABLE student_courses (
    student_id BIGINT NOT NULL REFERENCES students(id),
    course_id  BIGINT NOT NULL REFERENCES courses(id),
    enrolled_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY (student_id, course_id)
);

सुनहरे नियम:

  • कभी भी उस migration को edit न करें जो पहले से apply हो चुका है। परिवर्तन करने के लिए एक नया migration बनाएँ।
  • Deploy करने से पहले हमेशा production data की एक copy पर migrations का test करें।
  • Versioned migrations (एक बार चलते हैं) के लिए V और repeatable migrations (बदलने पर फिर से चलते हैं, views और stored procedures के लिए उपयोगी) के लिए R का उपयोग करें।

Database Indexing: Queries को तेज़ बनाना

एक index एक textbook के पीछे के index की तरह है। इसके बिना, आप "photosynthesis" खोजने के लिए हर page पलटते हैं। इसके साथ, आप index में "photosynthesis" देखते हैं और सीधे page 142 पर jump करते हैं।

Databases उसी तरह काम करते हैं। एक index के बिना, database हर row को scan करता है (एक "full table scan")। सही index के साथ, यह सीधे matching rows पर jump करता है।

B-Tree Index (Default)

B-tree default index type है। यह equality checks (=), range queries (<, >, BETWEEN), और sorting (ORDER BY) के लिए बहुत अच्छा काम करता है।

-- Single column index
CREATE INDEX idx_books_isbn ON books(isbn);

-- Use in JPA entity
@Entity
@Table(name = "books", indexes = {
    @Index(name = "idx_books_isbn", columnList = "isbn"),
    @Index(name = "idx_books_title", columnList = "title")
})
public class Book {
    // ...
}

Composite Index (कई Columns)

जब आप अक्सर कई columns के साथ एक साथ query करते हैं, तो एक composite index अलग single-column indexes की तुलना में कहीं अधिक कुशल है।

-- Composite index — column order matters!
CREATE INDEX idx_books_author_date ON books(author_id, published_date);

-- This index helps these queries:
-- WHERE author_id = 5                              (uses index)
-- WHERE author_id = 5 AND published_date > '2024-01-01' (uses index)
-- WHERE published_date > '2024-01-01'              (does NOT use index!)

-- Rule: the index follows the leftmost prefix rule.
-- Think of a phone book sorted by last name, then first name.
-- You can look up "Smith" quickly (last name first).
-- You can look up "Smith, John" quickly (both columns).
-- You CANNOT look up just "John" quickly (skipping the first column).

Partial Index (Conditional Index)

उन rows को क्यों index करें जिन्हें आप कभी query नहीं करते? एक partial index केवल उन rows को cover करता है जो एक condition से match करती हैं, जिससे यह छोटा और तेज़ हो जाता है।

-- Only index published books (skip drafts)
CREATE INDEX idx_books_published ON books(title)
    WHERE status = 'PUBLISHED';

-- Only index active users
CREATE INDEX idx_users_active_email ON users(email)
    WHERE is_active = true;

-- Partial indexes are smaller, faster to update, and use less disk space.
-- Perfect when you always filter by a specific condition.

GIN Index (Full-Text Search)

GIN (Generalized Inverted Index) text, arrays, और JSON के भीतर खोजने के लिए designed है। यह PostgreSQL full-text search की रीढ़ है।

-- Full-text search index
CREATE INDEX idx_books_search ON books
    USING GIN (to_tsvector('english', title || ' ' || description));

-- Query using the index
SELECT * FROM books
WHERE to_tsvector('english', title || ' ' || description)
      @@ to_tsquery('english', 'java & programming');

-- JSON index (for JSONB columns)
CREATE INDEX idx_books_metadata ON books USING GIN (metadata);

-- Query JSON with index support
SELECT * FROM books WHERE metadata @> '{"genre": "fiction"}'::jsonb;

Indexing के लिए Best Practices

  • WHERE, JOIN, और ORDER BY clauses में columns को index करें — वे columns हैं जिन्हें database खोजता है और sort करता है।
  • अधिक index न करें — हर index INSERT, UPDATE, और DELETE को धीमा कर देता है क्योंकि index को भी update करना पड़ता है।
  • EXPLAIN ANALYZE का उपयोग करें — हमेशा check करें कि क्या आपकी query वास्तव में index का उपयोग करती है। Database इसे ignore कर सकता है यदि table छोटी है।
  • Index usage को monitor करें — उन indexes को drop करें जिनका कोई उपयोग नहीं करता। वे disk बर्बाद करते हैं और किसी भी चीज़ के लिए writes को धीमा कर देते हैं।
-- Check if your query uses the index
EXPLAIN ANALYZE SELECT * FROM books WHERE isbn = '978-0134685991';

-- Find unused indexes in PostgreSQL
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;

सब कुछ एक साथ रखना

यहाँ एक पूर्ण उदाहरण है जो relationships के साथ एक entity, mixed query types के साथ एक repository, और एक transactional service method दिखाता है:

// Entity with relationship and index
@Entity
@Table(name = "orders", indexes = {
    @Index(name = "idx_orders_customer", columnList = "customer_id"),
    @Index(name = "idx_orders_date", columnList = "orderDate")
})
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    private LocalDateTime orderDate;
    private BigDecimal totalAmount;
}

// Repository with derived + JPQL + native queries
public interface OrderRepository extends JpaRepository<Order, Long> {

    // Derived query
    List<Order> findByCustomerIdOrderByOrderDateDesc(Long customerId);

    // JPQL with JOIN FETCH (solves N+1)
    @Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items WHERE o.customer.id = :customerId")
    List<Order> findByCustomerWithItems(@Param("customerId") Long customerId);

    // DTO projection
    @Query("SELECT new com.example.dto.OrderSummary(o.id, o.totalAmount, o.orderDate) " +
           "FROM Order o WHERE o.customer.id = :customerId")
    List<OrderSummary> findOrderSummaries(@Param("customerId") Long customerId);
}

// Transactional service
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;

    @Transactional
    public Order placeOrder(Long customerId, List<OrderItemRequest> items) {
        Order order = new Order();
        order.setCustomer(customerRepository.getReferenceById(customerId));
        order.setOrderDate(LocalDateTime.now());

        BigDecimal total = BigDecimal.ZERO;
        for (OrderItemRequest item : items) {
            inventoryService.deductStock(item.productId(), item.quantity());
            OrderItem orderItem = new OrderItem(item.productId(), item.quantity(), item.price());
            order.getItems().add(orderItem);
            orderItem.setOrder(order);
            total = total.add(item.price().multiply(BigDecimal.valueOf(item.quantity())));
        }
        order.setTotalAmount(total);

        return orderRepository.save(order);
        // If deductStock or save throws, EVERYTHING rolls back
    }
}

अक्सर पूछे जाने वाले प्रश्न

1. क्या मुझे production में Hibernate के auto DDL generation (hbm2ddl.auto) का उपयोग करना चाहिए?

कभी नहीं। Hibernate का auto DDL (spring.jpa.hibernate.ddl-auto=update) त्वरित prototyping के लिए ठीक है, लेकिन यह columns का नाम बदलने, data migrate करने, या partial indexes जोड़ने जैसे जटिल schema परिवर्तनों को नहीं संभाल सकता। Production में, हमेशा Flyway या Liquibase जैसे migration tool का उपयोग करें। ये tools आपको version-controlled, repeatable, और auditable schema परिवर्तन देते हैं। इसे इस तरह सोचें: आप Git के बिना code deploy नहीं करेंगे, इसलिए migration tool के बिना schema परिवर्तन deploy न करें।

2. मुझे FetchType.EAGER बनाम FetchType.LAZY का उपयोग कब करना चाहिए?

लगभग हमेशा LAZY का उपयोग करें। Eager fetching हर बार जब आप entity load करते हैं तब related data load करता है, भले ही आपको इसकी आवश्यकता न हो। यदि एक Author के पास 500 books हैं और आप बस author का नाम चाहते हैं, तो EAGER loading बेकार में सभी 500 books fetch करती है। LAZY को default के रूप में उपयोग करें और JOIN FETCH, @EntityGraph, या DTO projections का उपयोग करके आवश्यकता पड़ने पर selectively relationships load करें। केवल अपवाद @ManyToOne और @OneToOne है जहाँ related entity छोटी है और लगभग हमेशा आवश्यक होती है।

3. JpaRepository.save() और saveAndFlush() के बीच क्या अंतर है?

save() entity को "persist किया जाना है" के रूप में mark करता है लेकिन Hibernate तय करता है कि SQL को वास्तव में database में कब भेजना है (आमतौर पर transaction commit पर)। saveAndFlush() SQL को तुरंत भेजने के लिए मजबूर करता है। जब आपको database-generated ID तुरंत चाहिए (उदाहरण के लिए, इसे दूसरी service को pass करने के लिए) या जब आप transaction समाप्त होने से पहले एक database constraint violation पकड़ना चाहते हैं तो saveAndFlush() का उपयोग करें। अधिकांश मामलों के लिए, save() पर्याप्त है और अधिक कुशल है क्योंकि Hibernate कई saves को कम SQL statements में batch कर सकता है।

4. मैं JPQL और native SQL queries के बीच कैसे चुनूँ?

JPQL का उपयोग तब करें जब आपकी query विभिन्न databases में काम करती है और database-specific features की आवश्यकता नहीं है। JPQL entity names और fields पर operate करती है, इसलिए यह database-agnostic है। Native SQL का उपयोग तब करें जब आपको window functions (RANK, ROW_NUMBER), common table expressions (CTEs), database-specific hints जैसे features की आवश्यकता हो, या जब आपको अधिकतम performance चाहिए और generated SQL पर पूर्ण नियंत्रण चाहिए। एक अच्छा नियम: derived queries से शुरू करें, method names लंबे होने पर JPQL पर जाएँ, और native SQL का उपयोग तभी करें जब JPQL जो आप चाहते हैं उसे express नहीं कर सकती।

5. मुझे एक table पर कितने indexes बनाने चाहिए?

कोई निश्चित नियम नहीं है, लेकिन हर index की एक लागत होती है। प्रत्येक index reads को तेज़ करता है लेकिन writes को धीमा कर देता है क्योंकि database को प्रत्येक INSERT, UPDATE, और DELETE पर index को update करना होता है। एक सामान्य guideline: अपनी सबसे आम queries में WHERE clauses, JOIN conditions, और ORDER BY clauses में दिखने वाले columns को index करें। यह verify करने के लिए EXPLAIN ANALYZE का उपयोग करें कि आपकी queries वास्तव में indexes का उपयोग करती हैं। pg_stat_user_indexes के साथ index usage को monitor करें और शून्य scans वाले indexes को drop करें। एक सामान्य table के लिए, 3-5 अच्छी तरह से चुने गए indexes आमतौर पर पर्याप्त होते हैं। Over-indexing under-indexing जितना ही बुरा है।

More from Innovation

Innovation

इस साल बुद्धिमत्ता सस्ती हो गई — और मूल्य उस ओर खिसक गया जो आप उससे बनाते हैं

एक करोड़ AI टोकन की लागत एक ही साल में लगभग चौगुनी गिर गई। जब कच्चा माल इतनी तेज़ी से इतना सस्ता हो जाए, तो बढ़त मॉडल के मालिक होने की नहीं, बल्कि उपयोगी चीज़ बनाने वाले की होती है।

Jun 14, 20265 min read
Innovation

वह साल जब रोबोट ने प्रदर्शन करना बंद किया और काम करना शुरू किया

एक दशक तक ह्यूमनॉइड रोबोट सिर्फ़ डेमो वीडियो थे — हमेशा प्रभावशाली, हमेशा पाँच साल दूर। 2026 में कहानी चुपचाप वीडियो से उत्पादन संख्या में बदल गई। यहाँ बताया गया है कि वास्तव में क्या बदला।

Jun 14, 20265 min read
Innovation

AI हर महीने सस्ता हो रहा है। फिर सबके AI बिल क्यों फट रहे हैं?

AI की कीमत एक साल में करीब 4 गुना गिरी — जो काम $20 में होता था वह अब सेंट्स में। फिर भी कंपनियां सालभर का AI बजट महीनों में फूंक रही हैं। दोनों बातें सच हैं, और वजह समझना ज़रूरी है।

Jun 12, 20264 min read