この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

fetch joinの結果をフィルタする

例えば次のような1:Nのモデルがあったとします。


エンティティクラスは次のような感じですかね。

@Entity
public class Shop {

    @Id
    private long id;

    private String shopName;

    @OneToMany
    private Set<Employee> employees;

    ...
}


@Entity
public class Employee {

    @Id
    private long id;

    private String status;

    @ManyToOne
    private Shop shop;

    ...
}


このとき、例えば

  • 店舗名(Shop#shopName)が"東京"から始まる店舗エンティティを取得したい
  • でも、従業員(Shop#employees)は状態(Employee#status)が"ACTIVE"であるものだけをセットしたい

というニーズがあった場合にどうするか。
JPAの標準機能では、fetch joinするエンティティについて条件を指定することはできません。要するに、次のような構文は存在しない、ということです。

SELECT OBJECT(shop) FROM Shop AS shop 
    FETCH JOIN 
        shop.employees emp OF emp.status = 'ACTIVE'  ← こんな構文はない
    WHERE
        shop.shopName LIKE '東京%'


ちょっと悩んでいたのですが、ここでヒントを得てやり方が判りました。
ここは逆転の発想で、Employeeを検索しに行けばいいのです。
つまりこういうコトです。

SELECT OBJECT(emp) FROM Employee AS emp
    FETCH JOIN
        employee.shop shop
    WHERE
        emp.status = 'ACTIVE' AND
        shop.shopName LIKE '東京%'


でもって、結果セットをJavaで再構成すればOK。

private static final String QL_STRING = 
    "SELECT OBJECT(emp) FROM Employee AS emp " +
    "FETCH " +
        "JOIN employee.shop shop " +
    "WHERE " +
        "emp.status = 'ACTIVE' AND " +
        "shop.shopName LIKE '東京%'";


public List<Shop> selectShops() {

    final Query query = getEntityManager().createQuery(QL_STRING);
    
    // 従業員を店舗で名寄せする
    final Map<Long, Shop> foundShops = new HashMap<Long, Shop>();
    
    for (Employee emp : query.getResultList()) {
        final Shop shop = emp.getShop();
        
        if (!foundShops.containsKey(shop.getId())) {
            //アタッチされたエンティティをいじると色々面倒なのでコピーしたものを作成。
            final Shop foundShop = createCopyOfShop(shop);
            
            foundShop.setEmployees(new HashSet<Employee>());
            foundShops.put(shop.getId(), foundShop);
        }
        
        final Shop foundShop = foundShops.get(shop.getId());
        foundShop.getEmployees().add(emp);
    }
    
    return new ArrayList<Shop>(foundShops.values());
}


あまりスマートじゃないけど、まあこれでいいか、という感じではありますが。

もっと良いやり方をご存じの方がいたら、ぜひ教えてください。