|
| 1 | +# Aggregate strategies |
| 2 | + |
| 3 | +```{contents} |
| 4 | +:depth: 4 |
| 5 | +``` |
| 6 | + |
| 7 | +The aggregate strategy defines how to construct an entity aggregate from an arbitrary SELECT statement. |
| 8 | +It provides a structured way to map relational query results into hierarchical entity structures by specifying |
| 9 | +how entities should be linked together. |
| 10 | + |
| 11 | +## Aggregate strategy definition |
| 12 | + |
| 13 | +An aggregate strategy is defined by annotating an interface with `@AggregateStrategy`. |
| 14 | +This annotation specifies how an entity aggregate is reconstructed from a query result. |
| 15 | + |
| 16 | +```java |
| 17 | +@AggregateStrategy(root = Department.class, tableAlias = "d") |
| 18 | +interface DepartmentAggregateStrategy { |
| 19 | + ... |
| 20 | +} |
| 21 | +``` |
| 22 | + |
| 23 | +- The `root` element specifies the entity class that serves as the root of the aggregate. |
| 24 | +- The `tableAlias` element defines the alias for the table corresponding to the root entity class. |
| 25 | + This alias must be used in the SELECT statement to correctly map query results to entity properties. |
| 26 | + |
| 27 | +## Association linker definition |
| 28 | + |
| 29 | +An aggregate strategy must contain at least one field of type `BiConsumer` or `BiFunction`, |
| 30 | +annotated with `@AssociationLinker`. |
| 31 | +These functions are responsible for dynamically associating two entity instances. |
| 32 | +Use a `BiFunction` when associating immutable entities. |
| 33 | +For mutable entities, you may use either a `BiConsumer` or a `BiFunction`. |
| 34 | + |
| 35 | +```java |
| 36 | +@AggregateStrategy(root = Department.class, tableAlias = "d") |
| 37 | +interface DepartmentAggregateStrategy { |
| 38 | + @AssociationLinker(propertyPath = "employees", tableAlias = "e") |
| 39 | + BiConsumer<Department, Employee> employees = |
| 40 | + (d, e) -> { |
| 41 | + d.getEmployees().add(e); |
| 42 | + e.setDepartment(d); |
| 43 | + }; |
| 44 | + |
| 45 | + @AssociationLinker(propertyPath = "employees.address", tableAlias = "a") |
| 46 | + BiFunction<Employee, Address, Employee> address = |
| 47 | + (e, a) -> { |
| 48 | + e.setAddress(a); |
| 49 | + return e; |
| 50 | + }; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +- The first type parameter of a `BiConsumer` or `BiFunction` represents the type of the property owner, |
| 55 | + and the second type parameter represents the type of the property. |
| 56 | + The third type parameter of a `BiFunction` must be the same as the first one and represents the type of |
| 57 | + the entity after the association is applied. |
| 58 | +- The `propertyPath` element specifies the name of the target property as a dot-separated path from the root entity class. |
| 59 | +- The `tableAlias` element specifies the alias for the table corresponding to the entity class used as the second |
| 60 | + type parameter of the `BiConsumer` or `BiFunction`. This alias must be used in the SELECT statement. |
| 61 | + |
| 62 | +## Example |
| 63 | + |
| 64 | +The `DepartmentAggregateStrategy` described above is based on the following entity definitions: |
| 65 | + |
| 66 | +```java |
| 67 | +@Entity(naming = NamingType.SNAKE_LOWER_CASE) |
| 68 | +public class Department { |
| 69 | + @Id Integer id; |
| 70 | + String name; |
| 71 | + @Association List<Employee> employees = new ArrayList<>(); |
| 72 | + |
| 73 | + // getter, setter |
| 74 | +} |
| 75 | + |
| 76 | +@Entity(naming = NamingType.SNAKE_LOWER_CASE) |
| 77 | +public class Employee { |
| 78 | + @Id Integer id; |
| 79 | + String name; |
| 80 | + Integer departmentId; |
| 81 | + Integer addressId; |
| 82 | + @Association Department department; |
| 83 | + @Association Address address; |
| 84 | + |
| 85 | + // getter, setter |
| 86 | +} |
| 87 | + |
| 88 | +@Entity(naming = NamingType.SNAKE_LOWER_CASE) |
| 89 | +public class Address { |
| 90 | + @Id Integer id; |
| 91 | + String street; |
| 92 | + |
| 93 | + // getter, setter |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +In entity classes, association properties must be annotated with `@Association`. |
| 98 | +These properties can be linked using `@AssociationLinker`. |
| 99 | + |
| 100 | +### Using an aggregate strategy |
| 101 | + |
| 102 | +`DepartmentAggregateStrategy` is used by specifying it in the `aggregateStrategy` element of `@Select`: |
| 103 | + |
| 104 | +```java |
| 105 | +@Dao |
| 106 | +interface DepartmentDao { |
| 107 | + @Select(aggregateStrategy = DepartmentAggregateStrategy.class) |
| 108 | + Department selectById(Integer id); |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +The `selectById` method requires a SELECT statement like the following: |
| 113 | + |
| 114 | +```sql |
| 115 | +select |
| 116 | + d.id as d_id, |
| 117 | + d.name as d_name, |
| 118 | + a.id as a_id, |
| 119 | + a.street as a_street, |
| 120 | + e.id as e_id, |
| 121 | + e.name as e_name, |
| 122 | + e.department_id as e_department_id, |
| 123 | + e.address_id as e_address_id |
| 124 | +from |
| 125 | + department d |
| 126 | + left outer join |
| 127 | + employee e on (d.id = e.department_id) |
| 128 | + left outer join |
| 129 | + address a on (e.address_id = a.id) |
| 130 | +where |
| 131 | + d.id = /* id */0 |
| 132 | +``` |
| 133 | + |
| 134 | +:::{note} |
| 135 | +The SELECT list must include the IDs of all entities that form the aggregate. |
| 136 | +::: |
| 137 | + |
| 138 | +#### Column aliasing rules |
| 139 | + |
| 140 | +- Table aliases must match those defined in `DepartmentAggregateStrategy`. |
| 141 | +- Column aliases must begin with the table alias followed by an underscore (`_`). |
| 142 | + For example, `d.id` should be aliased as `d_id` and `e.id` as `e_id`. |
| 143 | + |
| 144 | +### Using the expansion directive |
| 145 | + |
| 146 | +By using the [expansion directive](sql.md#expansion-directive), the above SELECT statement can be written more concisely: |
| 147 | + |
| 148 | +```sql |
| 149 | +select |
| 150 | + /*%expand*/* |
| 151 | +from |
| 152 | + department d |
| 153 | + left outer join |
| 154 | + employee e on (d.id = e.department_id) |
| 155 | + left outer join |
| 156 | + address a on (e.address_id = a.id) |
| 157 | +where |
| 158 | + d.id = /* id */0 |
| 159 | +``` |
| 160 | + |
| 161 | +#### How expansion works |
| 162 | + |
| 163 | +- The `/*%expand*/*` directive automatically expands into a column list following predefined aliasing rules. |
| 164 | +- By default, all columns from all tables are included in the result set. |
| 165 | + |
| 166 | +To selectively expand only specific tables, pass a comma-separated list of table aliases: |
| 167 | + |
| 168 | +```sql |
| 169 | +select |
| 170 | + /*%expand "e, d" */*, |
| 171 | + a.id as a_id, |
| 172 | + a.street as a_street |
| 173 | +from |
| 174 | + department d |
| 175 | + left outer join |
| 176 | + employee e on (d.id = e.department_id) |
| 177 | + left outer join |
| 178 | + address a on (e.address_id = a.id) |
| 179 | +where |
| 180 | + d.id = /* id */0 |
| 181 | +``` |
| 182 | + |
| 183 | +- Here, only columns from tables `e` (`employee`) and `d` (`department`) are expanded. |
| 184 | +- The columns from table `a` (`address`) are explicitly specified. |
0 commit comments