How does nested property traversal work in a derived method, and why does findByAddress_City use an underscore?
answer
- camelCase path = JOIN
- greedy match then backtrack
- underscore = explicit path separator
- findByAddress_City
- avoid underscores in field names
basics
~20 sYou can navigate into related entities by camel-casing the path: findByAddressCity reaches the City field of the Address association (a JOIN). If a property name is ambiguous, an underscore (findByAddress_City) tells Spring exactly where to split the path.
solid answer
~40 sDerived methods can traverse associations/embeddables by concatenating property names, producing a JOIN. findByAddressCity means 'the city of the person's address'. The parser is greedy: it first tries to match the whole camel-cased segment as one property (addressCity), and only if that fails does it split into address.city. When both an addressCity property AND an address.city path could exist, the resolution is ambiguous, so Spring lets you insert an underscore as an explicit path separator: findByAddress_City forces the split at that point. The underscore is purely a disambiguation hint; it maps to the same JPA join. Best practice: use the underscore whenever traversing nested properties to make intent unambiguous and future-proof against a sibling property being added later.
code
java · 22 lines@Entity
class Person {
@Id Long id;
@ManyToOne Address address; // association -> JOIN
}
@Entity
class Address {
@Id Long id;
String city;
@ManyToOne Country country;
}
public interface PersonRepository extends JpaRepository<Person, Long> {
// Ambiguous if a scalar 'addressCity' ever exists:
List<Person> findByAddressCity(String city);
// Explicit & future-proof: property 'address', then 'city'
List<Person> findByAddress_City(String city);
// Deeper traversal: address.country.isoCode
List<Person> findByAddress_Country_IsoCode(String iso);
}go deeper
Know that camelCasing a path navigates into a related entity's field.
Explain greedy matching + backtracking and why the underscore disambiguates the path.
Distinguish embeddable (same-table) vs association (JOIN) traversal and the default INNER-join exclusion of null associations.
Recognize deep traversal as a design smell, the silent-remapping risk on new sibling properties, and when to move to @Query/EntityGraph for LEFT joins and fetch control.
## Traversing associations Entities often reference other entities (`@ManyToOne Address address`) or embeddables (`@Embedded Address address`). Derived methods can reach INTO those by chaining property names in the method name. `findByAddressCity(String city)` generates a query that JOINs to the address and filters on its `city` column. ## The greedy-matching algorithm When Spring parses `AddressCity` it does **not** immediately assume `address.city`. It follows this rule: 1. Take the longest possible segment and try to match it as a **single property** on the current type. So it first checks: does the root entity have a property `addressCity`? 2. If yes, that wins (equality on that property). 3. If no, it **backtracks**: peels off the last camel-hump and tries `address` as a property, then resolves `city` on that property's type. This greedy-then-backtrack behavior means a naming collision can silently pick the wrong interpretation. ## Why the underscore Because of the ambiguity, Spring Data supports the **underscore `_` as an explicit path separator**. `findByAddress_City` says unambiguously: property `address`, then `city` on it — never `addressCity`. The reference docs recommend using it for nested traversal. It has **no runtime cost** and compiles to the same JOIN. ### Important naming caveat Because underscore is reserved as the path separator, you should **not** put underscores in your Java field names (`address_line`). If you do, Spring's manual-vs-derived resolution gets confused. Standard camelCase field names avoid this. ## Embeddables vs associations - For an `@Embedded` value object, traversal reads columns on the SAME table (no join) — e.g. `city` is a column of the person table. - For a `@ManyToOne`/`@OneToOne` association, traversal issues an actual SQL JOIN to the related table. The method name looks identical either way; only the generated SQL differs. ## Depth You can go arbitrarily deep: `findByAddress_Country_IsoCode` → `address.country.isoCode`. Each hop is another join. Deep traversal is a smell — several joins hidden behind a name — and a candidate for an explicit `@Query`. ## Gotchas - Ambiguity: add a scalar `addressCity` later and `findByAddressCity` silently changes meaning; the underscore protects you. - Nullability: joining on a nullable `@ManyToOne` uses an INNER join by default in derived methods, so rows with a null address are excluded — you cannot force a LEFT join here without `@Query` or an EntityGraph. - N+1: filtering by a nested property joins, but does not fetch the association; accessing it later can still trigger lazy loads.
- If both a scalar property addressCity and an association path address.city could exist, which does findByAddressCity resolve to?The scalar addressCity wins — the parser greedily matches the longest whole-property name first, and only backtracks to the association split if no such property exists.
- Does nested traversal produce an INNER or LEFT join by default?An INNER join, so entities whose association is null are excluded. To get a LEFT join you must drop to @Query or use an EntityGraph.
saying these in an interview costs you the question
- Claiming the underscore changes the generated SQL or adds a join
- Saying camelCase traversal always splits at each hump (it greedily matches whole properties first)
- Assuming a nullable association gives a LEFT join