Tools: The Six Dimensions of Impedance Mismatch (Part 2 of 3): Granularity, Inheritance, and Identity

Tools: The Six Dimensions of Impedance Mismatch (Part 2 of 3): Granularity, Inheritance, and Identity

Source: Dev.to

Granularity: Fine-Grained Objects vs. Flat Tables ## A Common Example: Company and Address ## The JPA Solution: @embeddable ## Multiple Embedded Objects ## Inheritance: "Is-A" Relationships Meet SQL ## The Classic Example: Payment Methods ## Strategy 1: Table Per Class Hierarchy (SINGLE_TABLE) ## Strategy 2: Table Per Subclass (JOINED) ## Strategy 3: Table Per Concrete Class (TABLE_PER_CLASS) ## Strategy Comparison Table ## Identity: The "Identity Crisis" (The Most Critical Part) ## The Collection Problem ## The Solution: Override equals() and hashCode() ## Important Implementation Details First ## Approach 1: ID-Based Equality (Use Getters!) ## Approach 2: Business Key (Natural ID) ## Approach 3: UUID Primary Keys (Recommended for Modern Apps) ## The Symmetry Violation Problem ## Why This Matters ## What We've Learned Have you ever gotten a code review comment like this? "You need to override equals() and hashCode() in your entity classes. Without them, your Set operations won't work correctly across sessions." I remember getting this comment early in my career and thinking, "But it's just a simple Customer entity—why do I need to override those methods?" I added them to make the reviewer happy, but I didn't really understand why they mattered. Months later, I ran into a subtle bug: customers were appearing twice in my HashSet, even though they represented the same customer from the database. That's when it clicked—this wasn't just a code style preference. It was about understanding how JPA manages object identity. This is one of many issues that stem from the object-relational impedance mismatch—the fundamental incompatibility between how Java objects and relational databases represent data. In my last post, I introduced the impedance mismatch and explained why it exists. In this post, we're diving into three specific dimensions where this mismatch creates real problems: Granularity, Inheritance, and Identity. Each one represents a decision you'll face when designing your entities, and understanding the trade-offs will help you write better code and understand your code reviews. The next post will tackle Associations, Object Graph Navigation, and Polymorphism—where we'll address that infamous N+1 problem. Granularity refers to the relative size and complexity of the objects you're working with. The problem: Java allows fine-grained object composition—an Address object can contain a Country object, which might contain a Region object, and so on. You can nest objects as deeply as your domain model requires. In SQL, we're limited to tables (entities) and columns (attributes). Any deeper nesting requires additional tables and foreign keys. Consider a Company entity that has an Address. In Java, we'd naturally model this as two separate classes: But in SQL, there's no "Address" data type. We have several options: Option 1: Single address column (not recommended) Hard to query, hard to validate, hard to work with. Option 2: Flatten address into company table Works, but what if we need multiple addresses (billing vs. shipping)? Option 3: Separate address table More normalized, but now we need joins for every company query. Spring JPA gives us @Embeddable, which lets us have our cake and eat it too—modular Java code with clear separation of concerns, mapped to a flat table structure that SQL understands: This maps to the flattened table structure: What if a company has both a billing and shipping address? Use @AttributeOverrides: The key insight: With @Embeddable, we maintain clean domain modeling in Java while respecting the limitations of SQL's two-level granularity (tables and columns). In object-oriented programming, we use inheritance to represent "is-a" relationships: SQL has no native concept of inheritance. It only understands tables, columns, and relationships (foreign keys). So how do we persist an inheritance hierarchy? Each subclass has different data and different behavior. How do we store this in SQL tables? JPA provides three strategies, each with different trade-offs. Let's explore them. The idea: One table contains all columns for all classes in the hierarchy, with a discriminator column to identify the type. When to use: When you need fast polymorphic queries and have a relatively small, stable class hierarchy. The idea: One table for the base class, separate tables for each subclass, joined by foreign keys. When to use: When data integrity and normalization are priorities, and you can accept the performance cost of joins. The idea: Completely separate tables for each concrete class, duplicating parent class columns. When to use: Rarely. Only when you never need polymorphic queries and mostly work with specific concrete types. In practice: Most Spring Boot applications use SINGLE_TABLE for simplicity and performance, or JOINED when data integrity is critical. In Java, we have two ways to check if objects are "the same": In SQL, identity is defined by the primary key: The problem arises when we map objects to database rows. Consider this scenario: This becomes especially problematic with collections: Every JPA entity should override equals() and hashCode(). There are three main approaches, each with important implementation details. Before we look at the approaches, here are critical points that apply to all of them: 1. Use instanceof, not getClass() Why? Hibernate often returns proxy objects (runtime-generated subclasses) for lazy-loaded entities. Using getClass() would make a proxy and the real entity unequal, even though they represent the same database row. 2. Use getters, not direct field access Why? When other is a Hibernate proxy, accessing other.id directly might return null because the proxy hasn't been initialized yet. But other.getId() will trigger initialization and return the actual ID. 3. The hashCode() constant trade-off This returns the same hash for all instances of the class. Yes, this turns a HashSet into effectively a linked list (O(n) lookups instead of O(1)). This is intentional. Why? Because the hash must remain stable when an entity transitions from transient (no ID) to managed (has ID). If your hashCode() changes, entities already in a HashSet become unfindable. The trade-off: Stability over performance. For most applications, this is the right choice—collections of entities are usually small enough that O(n) performance is acceptable. When to use: Most applications, especially when you don't have a natural business key. When to use: When you have a genuinely immutable, unique business identifier (username, SSN, ISBN, etc.). When to use: Modern Spring Boot applications, especially microservices or distributed systems. This is increasingly becoming the default recommendation. You might be tempted to create a "hybrid" approach: The problem: This violates the symmetry contract of equals(). This breaks the equals() contract and causes unpredictable behavior in collections. The lesson: Pick one strategy and stick with it consistently. Without proper equals() and hashCode(): Bottom line: Always override equals() and hashCode() in your JPA entities. When in doubt, use UUIDs as your primary key strategy. We've covered three of the six impedance mismatch dimensions: Granularity: Solved with @Embeddable for composition without extra tables—letting us maintain clean domain models while respecting SQL's flat structure. Inheritance: Three strategies with different trade-offs: Choose based on your query patterns and data integrity requirements. Each of these represents a fundamental difference in how objects and relational databases model the world. JPA gives us tools to bridge the gap, but we need to understand the trade-offs to make good architectural decisions. Coming up next: Associations, Object Graph Navigation, and Polymorphism—where we'll tackle the N+1 problem, lazy loading gotchas, LazyInitializationException, and polymorphic queries. Key takeaway: Understanding these mismatches isn't just academic—it directly impacts your code reviews, performance optimization, and debugging sessions. The next time you see LazyInitializationException or get feedback about an N+1 problem, you'll understand why these issues exist and how to address them effectively. Additional Resources: Part of a series on understanding ORM fundamentals for modern Spring Boot developers. Based on lessons from "Hibernate in Action" (2005) synthesized with current best practices. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: public class Company { private Long id; private String name; private Address address; // Separate object } public class Address { private String street; private String city; private String state; private String zipCode; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: public class Company { private Long id; private String name; private Address address; // Separate object } public class Address { private String street; private String city; private String state; private String zipCode; } CODE_BLOCK: public class Company { private Long id; private String name; private Address address; // Separate object } public class Address { private String street; private String city; private String state; private String zipCode; } CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), address TEXT -- "123 Main St, Springfield, IL 62701" as a string ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), address TEXT -- "123 Main St, Springfield, IL 62701" as a string ); CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), address TEXT -- "123 Main St, Springfield, IL 62701" as a string ); CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), street VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zip_code VARCHAR(10) ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), street VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zip_code VARCHAR(10) ); CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), street VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zip_code VARCHAR(10) ); CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), address_id BIGINT, FOREIGN KEY (address_id) REFERENCES addresses(id) ); CREATE TABLE addresses ( id BIGINT PRIMARY KEY, street VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zip_code VARCHAR(10) ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), address_id BIGINT, FOREIGN KEY (address_id) REFERENCES addresses(id) ); CREATE TABLE addresses ( id BIGINT PRIMARY KEY, street VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zip_code VARCHAR(10) ); CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), address_id BIGINT, FOREIGN KEY (address_id) REFERENCES addresses(id) ); CREATE TABLE addresses ( id BIGINT PRIMARY KEY, street VARCHAR(255), city VARCHAR(255), state VARCHAR(2), zip_code VARCHAR(10) ); CODE_BLOCK: @Entity @Table(name = "companies") public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded private Address address; // Embedded, not a separate table! } @Embeddable public class Address { private String street; private String city; private String state; @Column(name = "zip_code") private String zipCode; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Table(name = "companies") public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded private Address address; // Embedded, not a separate table! } @Embeddable public class Address { private String street; private String city; private String state; @Column(name = "zip_code") private String zipCode; } CODE_BLOCK: @Entity @Table(name = "companies") public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded private Address address; // Embedded, not a separate table! } @Embeddable public class Address { private String street; private String city; private String state; @Column(name = "zip_code") private String zipCode; } CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), street VARCHAR(255), -- From Address city VARCHAR(255), -- From Address state VARCHAR(2), -- From Address zip_code VARCHAR(10) -- From Address ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), street VARCHAR(255), -- From Address city VARCHAR(255), -- From Address state VARCHAR(2), -- From Address zip_code VARCHAR(10) -- From Address ); CODE_BLOCK: CREATE TABLE companies ( id BIGINT PRIMARY KEY, name VARCHAR(255), street VARCHAR(255), -- From Address city VARCHAR(255), -- From Address state VARCHAR(2), -- From Address zip_code VARCHAR(10) -- From Address ); CODE_BLOCK: @Entity public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "billing_street")), @AttributeOverride(name = "city", column = @Column(name = "billing_city")), @AttributeOverride(name = "state", column = @Column(name = "billing_state")), @AttributeOverride(name = "zipCode", column = @Column(name = "billing_zip_code")) }) private Address billingAddress; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "shipping_street")), @AttributeOverride(name = "city", column = @Column(name = "shipping_city")), @AttributeOverride(name = "state", column = @Column(name = "shipping_state")), @AttributeOverride(name = "zipCode", column = @Column(name = "shipping_zip_code")) }) private Address shippingAddress; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "billing_street")), @AttributeOverride(name = "city", column = @Column(name = "billing_city")), @AttributeOverride(name = "state", column = @Column(name = "billing_state")), @AttributeOverride(name = "zipCode", column = @Column(name = "billing_zip_code")) }) private Address billingAddress; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "shipping_street")), @AttributeOverride(name = "city", column = @Column(name = "shipping_city")), @AttributeOverride(name = "state", column = @Column(name = "shipping_state")), @AttributeOverride(name = "zipCode", column = @Column(name = "shipping_zip_code")) }) private Address shippingAddress; } CODE_BLOCK: @Entity public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "billing_street")), @AttributeOverride(name = "city", column = @Column(name = "billing_city")), @AttributeOverride(name = "state", column = @Column(name = "billing_state")), @AttributeOverride(name = "zipCode", column = @Column(name = "billing_zip_code")) }) private Address billingAddress; @Embedded @AttributeOverrides({ @AttributeOverride(name = "street", column = @Column(name = "shipping_street")), @AttributeOverride(name = "city", column = @Column(name = "shipping_city")), @AttributeOverride(name = "state", column = @Column(name = "shipping_state")), @AttributeOverride(name = "zipCode", column = @Column(name = "shipping_zip_code")) }) private Address shippingAddress; } CODE_BLOCK: public abstract class BillingDetails { private Long id; private String owner; // Common behavior for all payment methods public abstract boolean validate(); } public class CreditCard extends BillingDetails { private String cardNumber; private String expiryDate; private String cvv; @Override public boolean validate() { // Luhn algorithm, expiry check, etc. } } public class BankAccount extends BillingDetails { private String accountNumber; private String routingNumber; private String bankName; @Override public boolean validate() { // Routing number validation, account verification, etc. } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: public abstract class BillingDetails { private Long id; private String owner; // Common behavior for all payment methods public abstract boolean validate(); } public class CreditCard extends BillingDetails { private String cardNumber; private String expiryDate; private String cvv; @Override public boolean validate() { // Luhn algorithm, expiry check, etc. } } public class BankAccount extends BillingDetails { private String accountNumber; private String routingNumber; private String bankName; @Override public boolean validate() { // Routing number validation, account verification, etc. } } CODE_BLOCK: public abstract class BillingDetails { private Long id; private String owner; // Common behavior for all payment methods public abstract boolean validate(); } public class CreditCard extends BillingDetails { private String cardNumber; private String expiryDate; private String cvv; @Override public boolean validate() { // Luhn algorithm, expiry check, etc. } } public class BankAccount extends BillingDetails { private String accountNumber; private String routingNumber; private String bankName; @Override public boolean validate() { // Routing number validation, account verification, etc. } } CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "billing_type", discriminatorType = DiscriminatorType.STRING) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String owner; } @Entity @DiscriminatorValue("CREDIT_CARD") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @DiscriminatorValue("BANK_ACCOUNT") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "billing_type", discriminatorType = DiscriminatorType.STRING) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String owner; } @Entity @DiscriminatorValue("CREDIT_CARD") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @DiscriminatorValue("BANK_ACCOUNT") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "billing_type", discriminatorType = DiscriminatorType.STRING) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String owner; } @Entity @DiscriminatorValue("CREDIT_CARD") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @DiscriminatorValue("BANK_ACCOUNT") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } CODE_BLOCK: CREATE TABLE billing_details ( id BIGINT PRIMARY KEY AUTO_INCREMENT, billing_type VARCHAR(20) NOT NULL, -- Discriminator: 'CREDIT_CARD' or 'BANK_ACCOUNT' owner VARCHAR(255), -- CreditCard fields (NULL for BankAccount rows) card_number VARCHAR(20), expiry_date VARCHAR(7), cvv VARCHAR(4), -- BankAccount fields (NULL for CreditCard rows) account_number VARCHAR(20), routing_number VARCHAR(9), bank_name VARCHAR(255) ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: CREATE TABLE billing_details ( id BIGINT PRIMARY KEY AUTO_INCREMENT, billing_type VARCHAR(20) NOT NULL, -- Discriminator: 'CREDIT_CARD' or 'BANK_ACCOUNT' owner VARCHAR(255), -- CreditCard fields (NULL for BankAccount rows) card_number VARCHAR(20), expiry_date VARCHAR(7), cvv VARCHAR(4), -- BankAccount fields (NULL for CreditCard rows) account_number VARCHAR(20), routing_number VARCHAR(9), bank_name VARCHAR(255) ); CODE_BLOCK: CREATE TABLE billing_details ( id BIGINT PRIMARY KEY AUTO_INCREMENT, billing_type VARCHAR(20) NOT NULL, -- Discriminator: 'CREDIT_CARD' or 'BANK_ACCOUNT' owner VARCHAR(255), -- CreditCard fields (NULL for BankAccount rows) card_number VARCHAR(20), expiry_date VARCHAR(7), cvv VARCHAR(4), -- BankAccount fields (NULL for CreditCard rows) account_number VARCHAR(20), routing_number VARCHAR(9), bank_name VARCHAR(255) ); CODE_BLOCK: -- Credit card row INSERT INTO billing_details (billing_type, owner, card_number, expiry_date, cvv, account_number, routing_number, bank_name) VALUES ('CREDIT_CARD', 'John Doe', '4532123456789012', '12/25', '123', NULL, NULL, NULL); -- Bank account row INSERT INTO billing_details (billing_type, owner, card_number, expiry_date, cvv, account_number, routing_number, bank_name) VALUES ('BANK_ACCOUNT', 'Jane Smith', NULL, NULL, NULL, '1234567890', '021000021', 'Chase Bank'); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: -- Credit card row INSERT INTO billing_details (billing_type, owner, card_number, expiry_date, cvv, account_number, routing_number, bank_name) VALUES ('CREDIT_CARD', 'John Doe', '4532123456789012', '12/25', '123', NULL, NULL, NULL); -- Bank account row INSERT INTO billing_details (billing_type, owner, card_number, expiry_date, cvv, account_number, routing_number, bank_name) VALUES ('BANK_ACCOUNT', 'Jane Smith', NULL, NULL, NULL, '1234567890', '021000021', 'Chase Bank'); CODE_BLOCK: -- Credit card row INSERT INTO billing_details (billing_type, owner, card_number, expiry_date, cvv, account_number, routing_number, bank_name) VALUES ('CREDIT_CARD', 'John Doe', '4532123456789012', '12/25', '123', NULL, NULL, NULL); -- Bank account row INSERT INTO billing_details (billing_type, owner, card_number, expiry_date, cvv, account_number, routing_number, bank_name) VALUES ('BANK_ACCOUNT', 'Jane Smith', NULL, NULL, NULL, '1234567890', '021000021', 'Chase Bank'); CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.JOINED) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String owner; } @Entity @Table(name = "credit_cards") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @Table(name = "bank_accounts") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.JOINED) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String owner; } @Entity @Table(name = "credit_cards") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @Table(name = "bank_accounts") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.JOINED) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String owner; } @Entity @Table(name = "credit_cards") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @Table(name = "bank_accounts") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } CODE_BLOCK: -- Parent table CREATE TABLE billing_details ( id BIGINT PRIMARY KEY AUTO_INCREMENT, owner VARCHAR(255) NOT NULL ); -- Child table for credit cards CREATE TABLE credit_cards ( id BIGINT PRIMARY KEY, card_number VARCHAR(20) NOT NULL, expiry_date VARCHAR(7) NOT NULL, cvv VARCHAR(4) NOT NULL, FOREIGN KEY (id) REFERENCES billing_details(id) ON DELETE CASCADE ); -- Child table for bank accounts CREATE TABLE bank_accounts ( id BIGINT PRIMARY KEY, account_number VARCHAR(20) NOT NULL, routing_number VARCHAR(9) NOT NULL, bank_name VARCHAR(255) NOT NULL, FOREIGN KEY (id) REFERENCES billing_details(id) ON DELETE CASCADE ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: -- Parent table CREATE TABLE billing_details ( id BIGINT PRIMARY KEY AUTO_INCREMENT, owner VARCHAR(255) NOT NULL ); -- Child table for credit cards CREATE TABLE credit_cards ( id BIGINT PRIMARY KEY, card_number VARCHAR(20) NOT NULL, expiry_date VARCHAR(7) NOT NULL, cvv VARCHAR(4) NOT NULL, FOREIGN KEY (id) REFERENCES billing_details(id) ON DELETE CASCADE ); -- Child table for bank accounts CREATE TABLE bank_accounts ( id BIGINT PRIMARY KEY, account_number VARCHAR(20) NOT NULL, routing_number VARCHAR(9) NOT NULL, bank_name VARCHAR(255) NOT NULL, FOREIGN KEY (id) REFERENCES billing_details(id) ON DELETE CASCADE ); CODE_BLOCK: -- Parent table CREATE TABLE billing_details ( id BIGINT PRIMARY KEY AUTO_INCREMENT, owner VARCHAR(255) NOT NULL ); -- Child table for credit cards CREATE TABLE credit_cards ( id BIGINT PRIMARY KEY, card_number VARCHAR(20) NOT NULL, expiry_date VARCHAR(7) NOT NULL, cvv VARCHAR(4) NOT NULL, FOREIGN KEY (id) REFERENCES billing_details(id) ON DELETE CASCADE ); -- Child table for bank accounts CREATE TABLE bank_accounts ( id BIGINT PRIMARY KEY, account_number VARCHAR(20) NOT NULL, routing_number VARCHAR(9) NOT NULL, bank_name VARCHAR(255) NOT NULL, FOREIGN KEY (id) REFERENCES billing_details(id) ON DELETE CASCADE ); CODE_BLOCK: -- Credit card INSERT INTO billing_details (id, owner) VALUES (1, 'John Doe'); INSERT INTO credit_cards (id, card_number, expiry_date, cvv) VALUES (1, '4532123456789012', '12/25', '123'); -- Bank account INSERT INTO billing_details (id, owner) VALUES (2, 'Jane Smith'); INSERT INTO bank_accounts (id, account_number, routing_number, bank_name) VALUES (2, '1234567890', '021000021', 'Chase Bank'); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: -- Credit card INSERT INTO billing_details (id, owner) VALUES (1, 'John Doe'); INSERT INTO credit_cards (id, card_number, expiry_date, cvv) VALUES (1, '4532123456789012', '12/25', '123'); -- Bank account INSERT INTO billing_details (id, owner) VALUES (2, 'Jane Smith'); INSERT INTO bank_accounts (id, account_number, routing_number, bank_name) VALUES (2, '1234567890', '021000021', 'Chase Bank'); CODE_BLOCK: -- Credit card INSERT INTO billing_details (id, owner) VALUES (1, 'John Doe'); INSERT INTO credit_cards (id, card_number, expiry_date, cvv) VALUES (1, '4532123456789012', '12/25', '123'); -- Bank account INSERT INTO billing_details (id, owner) VALUES (2, 'Jane Smith'); INSERT INTO bank_accounts (id, account_number, routing_number, bank_name) VALUES (2, '1234567890', '021000021', 'Chase Bank'); COMMAND_BLOCK: // Fetch a specific credit card - requires JOIN entityManager.find(CreditCard.class, 1L); // SQL generated: // SELECT bd.id, bd.owner, cc.card_number, cc.expiry_date, cc.cvv // FROM billing_details bd // INNER JOIN credit_cards cc ON bd.id = cc.id // WHERE bd.id = 1; // Fetch all billing details polymorphically - requires multiple JOINs List<BillingDetails> all = entityManager .createQuery("SELECT b FROM BillingDetails b", BillingDetails.class) .getResultList(); // SQL generated: // SELECT bd.id, bd.owner, // cc.card_number, cc.expiry_date, cc.cvv, // ba.account_number, ba.routing_number, ba.bank_name // FROM billing_details bd // LEFT JOIN credit_cards cc ON bd.id = cc.id // LEFT JOIN bank_accounts ba ON bd.id = ba.id; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Fetch a specific credit card - requires JOIN entityManager.find(CreditCard.class, 1L); // SQL generated: // SELECT bd.id, bd.owner, cc.card_number, cc.expiry_date, cc.cvv // FROM billing_details bd // INNER JOIN credit_cards cc ON bd.id = cc.id // WHERE bd.id = 1; // Fetch all billing details polymorphically - requires multiple JOINs List<BillingDetails> all = entityManager .createQuery("SELECT b FROM BillingDetails b", BillingDetails.class) .getResultList(); // SQL generated: // SELECT bd.id, bd.owner, // cc.card_number, cc.expiry_date, cc.cvv, // ba.account_number, ba.routing_number, ba.bank_name // FROM billing_details bd // LEFT JOIN credit_cards cc ON bd.id = cc.id // LEFT JOIN bank_accounts ba ON bd.id = ba.id; COMMAND_BLOCK: // Fetch a specific credit card - requires JOIN entityManager.find(CreditCard.class, 1L); // SQL generated: // SELECT bd.id, bd.owner, cc.card_number, cc.expiry_date, cc.cvv // FROM billing_details bd // INNER JOIN credit_cards cc ON bd.id = cc.id // WHERE bd.id = 1; // Fetch all billing details polymorphically - requires multiple JOINs List<BillingDetails> all = entityManager .createQuery("SELECT b FROM BillingDetails b", BillingDetails.class) .getResultList(); // SQL generated: // SELECT bd.id, bd.owner, // cc.card_number, cc.expiry_date, cc.cvv, // ba.account_number, ba.routing_number, ba.bank_name // FROM billing_details bd // LEFT JOIN credit_cards cc ON bd.id = cc.id // LEFT JOIN bank_accounts ba ON bd.id = ba.id; CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.TABLE) // Note: TABLE strategy private Long id; private String owner; } @Entity @Table(name = "credit_cards") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @Table(name = "bank_accounts") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.TABLE) // Note: TABLE strategy private Long id; private String owner; } @Entity @Table(name = "credit_cards") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @Table(name = "bank_accounts") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } CODE_BLOCK: @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public abstract class BillingDetails { @Id @GeneratedValue(strategy = GenerationType.TABLE) // Note: TABLE strategy private Long id; private String owner; } @Entity @Table(name = "credit_cards") public class CreditCard extends BillingDetails { @Column(name = "card_number") private String cardNumber; @Column(name = "expiry_date") private String expiryDate; private String cvv; } @Entity @Table(name = "bank_accounts") public class BankAccount extends BillingDetails { @Column(name = "account_number") private String accountNumber; @Column(name = "routing_number") private String routingNumber; @Column(name = "bank_name") private String bankName; } CODE_BLOCK: -- No parent table! Each concrete class has its own complete table CREATE TABLE credit_cards ( id BIGINT PRIMARY KEY, owner VARCHAR(255) NOT NULL, -- Duplicated from parent card_number VARCHAR(20) NOT NULL, expiry_date VARCHAR(7) NOT NULL, cvv VARCHAR(4) NOT NULL ); CREATE TABLE bank_accounts ( id BIGINT PRIMARY KEY, owner VARCHAR(255) NOT NULL, -- Duplicated from parent account_number VARCHAR(20) NOT NULL, routing_number VARCHAR(9) NOT NULL, bank_name VARCHAR(255) NOT NULL ); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: -- No parent table! Each concrete class has its own complete table CREATE TABLE credit_cards ( id BIGINT PRIMARY KEY, owner VARCHAR(255) NOT NULL, -- Duplicated from parent card_number VARCHAR(20) NOT NULL, expiry_date VARCHAR(7) NOT NULL, cvv VARCHAR(4) NOT NULL ); CREATE TABLE bank_accounts ( id BIGINT PRIMARY KEY, owner VARCHAR(255) NOT NULL, -- Duplicated from parent account_number VARCHAR(20) NOT NULL, routing_number VARCHAR(9) NOT NULL, bank_name VARCHAR(255) NOT NULL ); CODE_BLOCK: -- No parent table! Each concrete class has its own complete table CREATE TABLE credit_cards ( id BIGINT PRIMARY KEY, owner VARCHAR(255) NOT NULL, -- Duplicated from parent card_number VARCHAR(20) NOT NULL, expiry_date VARCHAR(7) NOT NULL, cvv VARCHAR(4) NOT NULL ); CREATE TABLE bank_accounts ( id BIGINT PRIMARY KEY, owner VARCHAR(255) NOT NULL, -- Duplicated from parent account_number VARCHAR(20) NOT NULL, routing_number VARCHAR(9) NOT NULL, bank_name VARCHAR(255) NOT NULL ); COMMAND_BLOCK: // Fetch specific type - simple, no joins entityManager.find(CreditCard.class, 1L); // SELECT id, owner, card_number, expiry_date, cvv FROM credit_cards WHERE id = 1; // Fetch polymorphically - requires UNION List<BillingDetails> all = entityManager .createQuery("SELECT b FROM BillingDetails b", BillingDetails.class) .getResultList(); // SQL generated (inefficient!): // SELECT id, owner, card_number, expiry_date, cvv, NULL as account_number, NULL as routing_number, NULL as bank_name // FROM credit_cards // UNION ALL // SELECT id, owner, NULL, NULL, NULL, account_number, routing_number, bank_name // FROM bank_accounts; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Fetch specific type - simple, no joins entityManager.find(CreditCard.class, 1L); // SELECT id, owner, card_number, expiry_date, cvv FROM credit_cards WHERE id = 1; // Fetch polymorphically - requires UNION List<BillingDetails> all = entityManager .createQuery("SELECT b FROM BillingDetails b", BillingDetails.class) .getResultList(); // SQL generated (inefficient!): // SELECT id, owner, card_number, expiry_date, cvv, NULL as account_number, NULL as routing_number, NULL as bank_name // FROM credit_cards // UNION ALL // SELECT id, owner, NULL, NULL, NULL, account_number, routing_number, bank_name // FROM bank_accounts; COMMAND_BLOCK: // Fetch specific type - simple, no joins entityManager.find(CreditCard.class, 1L); // SELECT id, owner, card_number, expiry_date, cvv FROM credit_cards WHERE id = 1; // Fetch polymorphically - requires UNION List<BillingDetails> all = entityManager .createQuery("SELECT b FROM BillingDetails b", BillingDetails.class) .getResultList(); // SQL generated (inefficient!): // SELECT id, owner, card_number, expiry_date, cvv, NULL as account_number, NULL as routing_number, NULL as bank_name // FROM credit_cards // UNION ALL // SELECT id, owner, NULL, NULL, NULL, account_number, routing_number, bank_name // FROM bank_accounts; CODE_BLOCK: Customer c1 = new Customer("John", "[email protected]"); Customer c2 = new Customer("John", "[email protected]"); c1 == c2; // false (different objects in memory) c1.equals(c2); // depends on equals() implementation Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Customer c1 = new Customer("John", "[email protected]"); Customer c2 = new Customer("John", "[email protected]"); c1 == c2; // false (different objects in memory) c1.equals(c2); // depends on equals() implementation CODE_BLOCK: Customer c1 = new Customer("John", "[email protected]"); Customer c2 = new Customer("John", "[email protected]"); c1 == c2; // false (different objects in memory) c1.equals(c2); // depends on equals() implementation CODE_BLOCK: SELECT * FROM customers WHERE id = 123; -- Identity is the primary key value Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: SELECT * FROM customers WHERE id = 123; -- Identity is the primary key value CODE_BLOCK: SELECT * FROM customers WHERE id = 123; -- Identity is the primary key value CODE_BLOCK: // Same JPA session - object identity is preserved EntityManager em = entityManagerFactory.createEntityManager(); Customer c1 = em.find(Customer.class, 1L); Customer c2 = em.find(Customer.class, 1L); System.out.println(c1 == c2); // true! (Hibernate returns the same instance) // Different sessions - different object instances EntityManager em1 = entityManagerFactory.createEntityManager(); EntityManager em2 = entityManagerFactory.createEntityManager(); Customer c3 = em1.find(Customer.class, 1L); Customer c4 = em2.find(Customer.class, 1L); System.out.println(c3 == c4); // false! (different instances) System.out.println(c3.equals(c4)); // ??? depends on equals() implementation Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Same JPA session - object identity is preserved EntityManager em = entityManagerFactory.createEntityManager(); Customer c1 = em.find(Customer.class, 1L); Customer c2 = em.find(Customer.class, 1L); System.out.println(c1 == c2); // true! (Hibernate returns the same instance) // Different sessions - different object instances EntityManager em1 = entityManagerFactory.createEntityManager(); EntityManager em2 = entityManagerFactory.createEntityManager(); Customer c3 = em1.find(Customer.class, 1L); Customer c4 = em2.find(Customer.class, 1L); System.out.println(c3 == c4); // false! (different instances) System.out.println(c3.equals(c4)); // ??? depends on equals() implementation CODE_BLOCK: // Same JPA session - object identity is preserved EntityManager em = entityManagerFactory.createEntityManager(); Customer c1 = em.find(Customer.class, 1L); Customer c2 = em.find(Customer.class, 1L); System.out.println(c1 == c2); // true! (Hibernate returns the same instance) // Different sessions - different object instances EntityManager em1 = entityManagerFactory.createEntityManager(); EntityManager em2 = entityManagerFactory.createEntityManager(); Customer c3 = em1.find(Customer.class, 1L); Customer c4 = em2.find(Customer.class, 1L); System.out.println(c3 == c4); // false! (different instances) System.out.println(c3.equals(c4)); // ??? depends on equals() implementation COMMAND_BLOCK: @Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String email; // Using default equals() and hashCode() (BAD!) } // Later in code: Set<Customer> customers = new HashSet<>(); EntityManager em1 = entityManagerFactory.createEntityManager(); Customer c1 = em1.find(Customer.class, 1L); customers.add(c1); EntityManager em2 = entityManagerFactory.createEntityManager(); Customer c2 = em2.find(Customer.class, 1L); customers.contains(c2); // FALSE! Even though it's the same database row! customers.size(); // 2 if you add c2, even though they represent the same customer Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String email; // Using default equals() and hashCode() (BAD!) } // Later in code: Set<Customer> customers = new HashSet<>(); EntityManager em1 = entityManagerFactory.createEntityManager(); Customer c1 = em1.find(Customer.class, 1L); customers.add(c1); EntityManager em2 = entityManagerFactory.createEntityManager(); Customer c2 = em2.find(Customer.class, 1L); customers.contains(c2); // FALSE! Even though it's the same database row! customers.size(); // 2 if you add c2, even though they represent the same customer COMMAND_BLOCK: @Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String email; // Using default equals() and hashCode() (BAD!) } // Later in code: Set<Customer> customers = new HashSet<>(); EntityManager em1 = entityManagerFactory.createEntityManager(); Customer c1 = em1.find(Customer.class, 1L); customers.add(c1); EntityManager em2 = entityManagerFactory.createEntityManager(); Customer c2 = em2.find(Customer.class, 1L); customers.contains(c2); // FALSE! Even though it's the same database row! customers.size(); // 2 if you add c2, even though they represent the same customer CODE_BLOCK: // CORRECT - works with Hibernate proxies if (!(o instanceof Customer)) return false; // WRONG - fails with Hibernate proxies if (this.getClass() != o.getClass()) return false; Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // CORRECT - works with Hibernate proxies if (!(o instanceof Customer)) return false; // WRONG - fails with Hibernate proxies if (this.getClass() != o.getClass()) return false; CODE_BLOCK: // CORRECT - works with Hibernate proxies if (!(o instanceof Customer)) return false; // WRONG - fails with Hibernate proxies if (this.getClass() != o.getClass()) return false; CODE_BLOCK: // CORRECT - works with Hibernate proxies return getId() != null && Objects.equals(getId(), other.getId()); // WRONG - may return null for uninitialized proxies return id != null && Objects.equals(id, other.id); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // CORRECT - works with Hibernate proxies return getId() != null && Objects.equals(getId(), other.getId()); // WRONG - may return null for uninitialized proxies return id != null && Objects.equals(id, other.id); CODE_BLOCK: // CORRECT - works with Hibernate proxies return getId() != null && Objects.equals(getId(), other.getId()); // WRONG - may return null for uninitialized proxies return id != null && Objects.equals(id, other.id); CODE_BLOCK: @Override public int hashCode() { return getClass().hashCode(); // or return 31; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Override public int hashCode() { return getClass().hashCode(); // or return 31; } CODE_BLOCK: @Override public int hashCode() { return getClass().hashCode(); // or return 31; } CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; public Long getId() { return id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; // Works with proxies Customer other = (Customer) o; // Use getters to handle proxies correctly return getId() != null && Objects.equals(getId(), other.getId()); } @Override public int hashCode() { // Constant hash ensures stability across persistence states // Trade-off: O(n) HashSet performance for stability return getClass().hashCode(); } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; public Long getId() { return id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; // Works with proxies Customer other = (Customer) o; // Use getters to handle proxies correctly return getId() != null && Objects.equals(getId(), other.getId()); } @Override public int hashCode() { // Constant hash ensures stability across persistence states // Trade-off: O(n) HashSet performance for stability return getClass().hashCode(); } } CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; public Long getId() { return id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; // Works with proxies Customer other = (Customer) o; // Use getters to handle proxies correctly return getId() != null && Objects.equals(getId(), other.getId()); } @Override public int hashCode() { // Constant hash ensures stability across persistence states // Trade-off: O(n) HashSet performance for stability return getClass().hashCode(); } } CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; // Natural business key - must be immutable! private String name; public String getEmail() { return email; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // Use business key - works before AND after persistence return Objects.equals(getEmail(), other.getEmail()); } @Override public int hashCode() { // Hash based on business key return Objects.hash(email); } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; // Natural business key - must be immutable! private String name; public String getEmail() { return email; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // Use business key - works before AND after persistence return Objects.equals(getEmail(), other.getEmail()); } @Override public int hashCode() { // Hash based on business key return Objects.hash(email); } } CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; // Natural business key - must be immutable! private String name; public String getEmail() { return email; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // Use business key - works before AND after persistence return Objects.equals(getEmail(), other.getEmail()); } @Override public int hashCode() { // Hash based on business key return Objects.hash(email); } } CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") @Column(updatable = false, nullable = false) private UUID id; private String name; private String email; // Constructor assigns UUID immediately public Customer() { this.id = UUID.randomUUID(); } public UUID getId() { return id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // UUID is assigned at creation, so this always works return Objects.equals(getId(), other.getId()); } @Override public int hashCode() { // Can safely hash the UUID since it's assigned in constructor return Objects.hash(id); } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") @Column(updatable = false, nullable = false) private UUID id; private String name; private String email; // Constructor assigns UUID immediately public Customer() { this.id = UUID.randomUUID(); } public UUID getId() { return id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // UUID is assigned at creation, so this always works return Objects.equals(getId(), other.getId()); } @Override public int hashCode() { // Can safely hash the UUID since it's assigned in constructor return Objects.hash(id); } } CODE_BLOCK: @Entity @Table(name = "customers") public class Customer { @Id @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") @Column(updatable = false, nullable = false) private UUID id; private String name; private String email; // Constructor assigns UUID immediately public Customer() { this.id = UUID.randomUUID(); } public UUID getId() { return id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // UUID is assigned at creation, so this always works return Objects.equals(getId(), other.getId()); } @Override public int hashCode() { // Can safely hash the UUID since it's assigned in constructor return Objects.hash(id); } } CODE_BLOCK: // DON'T DO THIS - it violates equals() symmetry! @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // Use ID if both have IDs if (getId() != null && other.getId() != null) { return Objects.equals(getId(), other.getId()); } // Otherwise fall back to business key return Objects.equals(getEmail(), other.getEmail()); } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // DON'T DO THIS - it violates equals() symmetry! @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // Use ID if both have IDs if (getId() != null && other.getId() != null) { return Objects.equals(getId(), other.getId()); } // Otherwise fall back to business key return Objects.equals(getEmail(), other.getEmail()); } CODE_BLOCK: // DON'T DO THIS - it violates equals() symmetry! @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Customer)) return false; Customer other = (Customer) o; // Use ID if both have IDs if (getId() != null && other.getId() != null) { return Objects.equals(getId(), other.getId()); } // Otherwise fall back to business key return Objects.equals(getEmail(), other.getEmail()); } COMMAND_BLOCK: Customer c1 = new Customer("[email protected]"); // Transient, no ID yet entityManager.persist(c1); // Now has ID = 1 Customer c2 = new Customer("[email protected]"); // Transient, no ID c1.equals(c2); // Uses ID path -> false (one has ID, one doesn't) c2.equals(c1); // Uses business key path -> true (same email) // Symmetry violated! c1.equals(c2) != c2.equals(c1) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: Customer c1 = new Customer("[email protected]"); // Transient, no ID yet entityManager.persist(c1); // Now has ID = 1 Customer c2 = new Customer("[email protected]"); // Transient, no ID c1.equals(c2); // Uses ID path -> false (one has ID, one doesn't) c2.equals(c1); // Uses business key path -> true (same email) // Symmetry violated! c1.equals(c2) != c2.equals(c1) COMMAND_BLOCK: Customer c1 = new Customer("[email protected]"); // Transient, no ID yet entityManager.persist(c1); // Now has ID = 1 Customer c2 = new Customer("[email protected]"); // Transient, no ID c1.equals(c2); // Uses ID path -> false (one has ID, one doesn't) c2.equals(c1); // Uses business key path -> true (same email) // Symmetry violated! c1.equals(c2) != c2.equals(c1) COMMAND_BLOCK: Set<Customer> customers = new HashSet<>(); customers.add(customerFromSession1); customers.add(customerFromSession2); // Duplicate! Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: Set<Customer> customers = new HashSet<>(); customers.add(customerFromSession1); customers.add(customerFromSession2); // Duplicate! COMMAND_BLOCK: Set<Customer> customers = new HashSet<>(); customers.add(customerFromSession1); customers.add(customerFromSession2); // Duplicate! CODE_BLOCK: order.setCustomer(customerFromDifferentSession); // Hibernate might think this is a different customer! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: order.setCustomer(customerFromDifferentSession); // Hibernate might think this is a different customer! CODE_BLOCK: order.setCustomer(customerFromDifferentSession); // Hibernate might think this is a different customer! CODE_BLOCK: // Second-level cache might store duplicates Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // Second-level cache might store duplicates CODE_BLOCK: // Second-level cache might store duplicates CODE_BLOCK: customer.getOrders().contains(orderFromDifferentSession); // false! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: customer.getOrders().contains(orderFromDifferentSession); // false! CODE_BLOCK: customer.getOrders().contains(orderFromDifferentSession); // false! - A Dog is an Animal - A CreditCard is a BillingDetails - A Manager is an Employee - Simple schema (one table) - Fast polymorphic queries (no joins needed) - Fast inserts and updates - Easy to add new subclasses (just add columns) - Many NULL columns (denormalized) - Cannot enforce NOT NULL constraints on subclass-specific columns - Table can become very wide with many subclasses - Violates database normalization principles - Normalized schema (no NULLs except in polymorphic queries) - Can enforce NOT NULL constraints on all columns - Supports polymorphic associations cleanly - Easy to understand schema - Each table represents exactly one class - Requires joins for every query (slower reads) - More complex query execution plans - Multiple inserts required for one object - Can have performance issues with deep hierarchies - No NULLs (fully normalized for each type) - Fast queries for specific concrete types - Can enforce NOT NULL on all columns - Schema clearly shows concrete types - Polymorphic queries are very slow (UNION across all tables) - Changes to parent class require schema updates across all tables - Identity management is complex (need to ensure uniqueness across tables) - Poor support for polymorphic associations - Code duplication in schema - Simple and straightforward - Works correctly with Hibernate proxies (via getter) - Uses the database's notion of identity - Only works after persistence (when ID is assigned) - Two new (unsaved) entities with identical data won't be equal - Can't add transient entities to a Set and expect to find them after persistence - Works for both transient and persistent entities - Matches business domain logic (two customers with same email are the same) - More intuitive for domain modeling - Requires truly immutable business key - if email changes, entity becomes unfindable in collections - Not all entities have natural business keys - Business rules may change (what if email isn't unique anymore?) - ID is assigned at object creation (not at persist time) - Works correctly for both transient and persistent entities - No need for business key - Works perfectly with HashSet and other collections - Distributed-system friendly (no database roundtrip for ID generation) - UUIDs are larger than integers (16 bytes vs 4/8 bytes) - Slightly slower for database joins (though rarely noticeable) - Less human-readable in logs/debugging - Collections don't work correctly - Cascade operations can fail - Caching breaks - Lazy loading issues - Granularity: Solved with @Embeddable for composition without extra tables—letting us maintain clean domain models while respecting SQL's flat structure. - Inheritance: Three strategies with different trade-offs: SINGLE_TABLE: Fast, simple, but denormalized JOINED: Normalized, but requires joins TABLE_PER_CLASS: Rarely used, poor polymorphic query support - SINGLE_TABLE: Fast, simple, but denormalized - JOINED: Normalized, but requires joins - TABLE_PER_CLASS: Rarely used, poor polymorphic query support - SINGLE_TABLE: Fast, simple, but denormalized - JOINED: Normalized, but requires joins - TABLE_PER_CLASS: Rarely used, poor polymorphic query support - Identity: Always override equals() and hashCode() in entities. Use UUIDs for simple, robust identity management, or business keys when you have truly immutable natural identifiers. - JPA Inheritance Mapping Strategies - Hibernate equals() and hashCode() Best Practices - UUID vs Auto-Increment IDs