In a POJO, you can use @JoinFetch
on a field or class to filter associated entities. The difference from @Join
is that this allows fetching all lazy-related data at once.
Single Join Fetch
For example, consider a Customer entity that has a one-to-many relationship with an Order entity:
@Entity
class Customer {
@OneToMany(fetch = LAZY, cascade = ALL)
@JoinColumn(name = "order_id")
private Collection<Order> orders;
}
@Entity
class Order {
private String itemName;
}
If you want to fetch Order data when retrieving Customer, you can do:
@Data
@JoinFetch(path = "orders")
class CustomerOrderCriteria {
@Spec
String name;
}
The executed SQL will be like:
select distinct
customer0_.* ...,
orders1_.* ...
from customer customer0_
inner outer join orders orders1_ on customer0_.id=orders1_.order_id
where customer0_.name=?
You can see that
orders1_.*
is also included in the select fields.
Join Behavior
To better align with most usage scenarios, the default behavior is as follows:
- The default join type is
INNER
. - Duplicate results are removed (
distinct
).
You can modify the default behavior by configuring @JoinFetch#joinType
or @JoinFetch#distinct
, for example:
@JoinFetch(joinType = JoinType.RIGHT, distinct = false)
With Clause
@JoinFetch
also allows filtering data. For example, if you want to filter both customer names and order names, define a POJO as follows:
@Data
@JoinFetch(path = "orders", alias = "o")
public class CustomerOrderCriteria {
@Spec
String name;
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
}
The executed SQL will be like:
select distinct
customer0_.* ...,
orders1_.* ...
from customer customer0_
inner outer join orders orders1_ on customer0_.id=orders1_.order_id
where customer0_.name=? and orders1_.item_name in (?)
@JoinFetch
can also be used at the field level, for example:
@Data
public class CustomerOrderCriteria {
@Spec
String name;
@JoinFetch(path = "orders", alias = "o")
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
}
Multi Join Fetches
You can use @JoinFetches
to define multi-level fetches. For example, if the Order entity has a many-to-many relationship with a Tag entity:
@Entity
class Customer {
@OneToMany(cascade = ALL, fetch = LAZY)
@JoinColumn(name = "order_id")
private Set<Order> orders;
}
@Entity
class Order {
@ManyToMany(cascade = ALL, fetch = LAZY)
private Set<Tag> tags;
}
@Entity
class Tag {
private String name;
}
If you want to query customers who purchased items belonging to specific categories, define a POJO as follows:
@Data
class CustomerOrderTagCriteria {
@JoinFetches({
@JoinFetch(path = "orders", alias = "o"),
@JoinFetch(path = "o.tags", alias = "t")
})
@Spec(path = "t.name", value = In.class)
Collection<String> tags;
}
The executed SQL will be like:
select distinct
customer0_.* ...,
orders1_.* ...,
tag3_.* ...
from customer customer0_
inner join orders orders1_ on customer0_.id=orders1_.order_id
inner join orders_tags tags2_ on orders1_.id=tags2_.order_id
inner join tag tag3_ on tags2_.tags_id=tag3_.id
where tag3_.name in (?)
@JoinFetches
can also be used at the class level, making the aliases
available across fields in the same object. For example:
@Data
@JoinFetches({
@JoinFetch(path = "orders", alias = "o"),
@JoinFetch(path = "o.tags", alias = "t")
})
public class CustomerOrderCriteria {
@Spec(path = "o.itemName", value = In.class)
Collection<String> items;
@Spec(path = "t.name", value = In.class)
Collection<String> tags;
}
Repeatable
@JoinFetch
supports repeatable declarations and can be used multiple times directly instead of using @JoinFetches
:
@JoinFetches({
@JoinFetch(path = "orders", alias = "o"),
@JoinFetch(path = "o.tags", alias = "t")
})
public class CustomerOrderCriteria {}
This is equivalent to the following:
@JoinFetch(path = "orders", alias = "o")
@JoinFetch(path = "o.tags", alias = "t")
public class CustomerOrderCriteria {}
Fetches Order
Annotations are processed in order, so @JoinFetches
must be defined in the correct sequence.
For example, in the previous scenario, the following order is incorrect:
@Data
class CustomerOrderTagCriteria {
@JoinFetch(path = "o.tags", alias = "t") // "o" alias will be not exist during processing this @Join
@JoinFetch(path = "orders", alias = "o")
@Spec(path = "t.name", value = In.class)
Collection<String> tagNames;
}
Alias
The usage rules for @JoinFetch#alias
are as follows:
- If not provided, the default alias is
@JoinFetch#path
- If it contains
.
, it will be replaced with_
For example:
@JoinFetch(path = "orders") // default alias is "orders"
@JoinFetch(path = "orders.tags") // default alias is "orders_tags"
@Spec(path = "orders_tags.name", value = In.class)
Reusing the Same Path
If multiple fields declare @JoinFetch
on the same path,
only one join will be generated in the SQL, even if the aliases are different.
For example:
@Data
public class CustomerOrderCriteria {
@JoinFetch(path = "orders", alias = "o1") // alias = o1
@Spec(path = "o1.id")
Long orderId;
@JoinFetch(path = "orders", alias = "o2") // alias = o2
@Spec(path = "o2.itemName", value = Like.class)
String itemName;
}
or equivalently:
@Data
public class CustomerOrderCriteria {
@JoinFetch(path = "orders", alias = "o") // same alias
@Spec(path = "o.id")
Long orderId;
@JoinFetch(path = "orders", alias = "o") // same alias
@Spec(path = "o.itemName", value = Like.class)
String itemName;
}
The executed SQL will be like:
select distinct customer0_.*
from customer customer0_
inner join orders o1_ on customer0_.id = o1_.order_id -- single join reused
where o1_.id = ?
and o1_.item_name like ? -- both conditions on same join