skip to content

What are the trade-offs between named parameters, positional parameters, and safe binding in @Query, and how do you avoid injection?

level: middleimportance: should knowfreq 55%

answer

  1. :name + @Param = readable, order-independent
  2. ?1/?2 = 1-based, fragile to arg reordering
  3. both are bound → injection-safe
  4. always bind, never concatenate user input
  5. LIKE: put % in the value; IN :ids for collections

basics

~20 s

Named parameters (:name with @Param) are readable and refactor-safe; positional parameters (?1, ?2) are terser but tied to argument order. Both are bound safely by the driver, which prevents injection. Never concatenate user input into the query string.

solid answer

~40 s

Spring Data @Query supports two binding styles. **Named parameters** — `:email` in the query matched to a `@Param("email")` argument — are self-documenting, order-independent, and survive refactoring, so they're the default recommendation, especially when the same value appears multiple times. **Positional parameters** — `?1`, `?2` (1-based, by argument order) — are more concise but fragile: reordering arguments silently breaks the mapping. Both are true **bind parameters**: the value is sent separately to the database, never spliced into the query text, so they're immune to SQL/JPQL injection. The security rule is: **always bind, never concatenate**. String-building a query with user input (or interpolating it via text-rewriting SpEL `#{...}`) opens an injection hole. For collections use `IN :ids`; for LIKE, add wildcards to the argument value, not by concatenating into the query.

code

java · 14 lines
java
public interface UserRepository extends JpaRepository<User, Long> {

    // Preferred: named parameters
    @Query("SELECT u FROM User u WHERE u.email = :email AND u.tenant = :tenant")
    Optional<User> find(@Param("email") String email, @Param("tenant") String tenant);

    // Collection binding
    @Query("SELECT u FROM User u WHERE u.id IN :ids")
    List<User> findByIds(@Param("ids") Collection<Long> ids);

    // Safe LIKE: caller supplies the wildcards as the bound value
    @Query("SELECT u FROM User u WHERE u.name LIKE :pattern")
    List<User> search(@Param("pattern") String pattern);
}

go deeper

for a junior

Knows :name/@Param and ?1 exist and how to use them.

for a middle

Explains the trade-offs and that both styles bind safely to prevent injection.

for a senior

Adds collection/LIKE handling and the danger of text-rewriting SpEL vs bound :#{...}.

for a principal

Sets team conventions (named by default), reviews for injection, and reasons about maintainability at scale.

## The two binding styles Every `@Query` needs to pass method arguments into the query. Spring Data offers: ### Named parameters (preferred) A colon-prefixed name in the query, matched to a method argument annotated `@Param`: ```java @Query("SELECT u FROM User u WHERE u.email = :email AND u.tenant = :tenant") User find(@Param("email") String email, @Param("tenant") String tenant); ``` Benefits: - **Self-documenting** — the query reads clearly. - **Order-independent** — reordering method arguments doesn't break it. - **Reusable** — the same `:tenant` can appear multiple times bound to one argument. ### Positional parameters Numbered placeholders, **1-based**, mapped by argument order: ```java @Query("SELECT u FROM User u WHERE u.email = ?1 AND u.tenant = ?2") User find(String email, String tenant); ``` Benefits: terse, no `@Param` needed. Drawback: **fragile** — swap the arguments and the mapping silently changes, a latent bug. Prefer named parameters for anything beyond a trivial single argument. ### @Param name matching With `-parameters` compilation (default in Spring Boot), Spring can sometimes match by the raw argument name without `@Param`, but explicit `@Param` is the robust, recommended practice. ## Why binding is safe (injection immunity) Both styles produce **bind parameters**. The query text with placeholders is prepared once; the actual values travel to the database **separately** and are never parsed as query syntax. This is precisely what defeats **SQL/JPQL injection**: a value like `' OR '1'='1` is treated as literal data, not code. ### The anti-pattern to reject Never build the query by concatenating user input: ```java // DANGEROUS — do not do this String jpql = "SELECT u FROM User u WHERE u.email = '" + email + "'"; ``` Even inside `@Query`, avoid **text-rewriting SpEL** (`#{...}`) fed with user data — that interpolates into the query string and is injectable. When you need a value computed from an argument, use the **bound** SpEL form `:#{#arg.prop}` (the leading colon makes it a bind parameter). ## Collections and LIKE - **`IN` clauses**: bind a `Collection` directly — `WHERE u.id IN :ids` with `@Param("ids") List<Long> ids`. - **LIKE / wildcards**: put the `%` in the **argument value**, not by string-splicing: ```java @Query("SELECT u FROM User u WHERE u.name LIKE :pattern") List<User> search(@Param("pattern") String pattern); // caller passes "%" + term + "%" ``` Or use JPQL `CONCAT('%', :term, '%')` — still a bound parameter, still safe. ## Practical guidance - **Default to named parameters** for readability and refactor-safety. - Use **positional** only for very short queries where it clearly adds nothing to name them. - **Always bind**; never concatenate untrusted input, in `@Query` strings or dynamic query builders. - Be consistent within a codebase — mixing styles in one query is not allowed and hurts readability across methods.

  • Why are @Query bind parameters immune to SQL injection?
    The query text with placeholders is prepared separately from the values; the driver sends the values as data, never parsed as query syntax. A malicious string is treated as a literal, not executable code.
  • When would you still prefer positional over named parameters?
    Rarely — only for trivially short queries with one or two arguments where naming adds no clarity. Named parameters are the default because they're refactor-safe and self-documenting.

saying these in an interview costs you the question

  • Claiming positional parameters are 0-based
  • Concatenating user input into the query string 'because it's inside @Query'
  • Thinking named vs positional affects injection safety (both bind safely)
  • Adding LIKE wildcards by string concatenation instead of in the bound value

context