Collection Mapping
Collection Mapping
Learn how to map JOIN query results to nested data structures using mappingType=perRow
and
mappingType=collection
annotations.
Overview
SQLiteNow supports advanced mapping of JOIN query results into nested data structures. Instead of flat result objects, you can create rich domain models with embedded objects and collections.
Two mapping types are available:
perRow
: Maps joined data to nested objects, returning one row per JOIN result (flat structure). This is not really a collection mapping, but it is useful when you want to map joined data to nested objects.collection
: Groups joined data into collections, returning one row per main entity with nested collections
Flat Structure Mapping with ‘perRow’
Use mappingType=perRow
to map joined table data into nested objects while maintaining a flat row
structure. Each JOIN result becomes a separate row in your result set.
Example: Person with Addresses (Flat Structure)
File: queries/person/selectAllWithAddresses.sql
SELECT p.id,
p.first_name,
p.last_name,
a.id AS address_id,
a.street,
a.city
/* @@{ dynamicField=address,
mappingType=perRow,
propertyType=my.app.Address,
sourceTable=a
removeAliasPrefix=address_
} */
FROM Person p
LEFT JOIN PersonAddress a ON p.id = a.person_id
This generates:
object PersonQuery {
object SelectAllWithAddresses {
data class Result(
val id: Long,
val firstName: String,
val lastName: String,
// This is result of using annotations: dynamicField=address, sourceTable=a
// All columns with alias 'a.*' are mapped to this object
// This field can be null if there is no matching address
val address: my.app.Address?
)
}
}
Result structure: If a table has 1 person with 2 addresses, and 1 person with no addresses, you get 3 rows:
Row 1: person_1 | address = {... address_1}
Row 2: person_1 | address = {... address_2}
Row 3: person_2 | address = null
Each row contains the complete person data plus one address nested object.
Multiple perRow
mappings are possible in a single query.
Note that mappingType=perRow
(or collection
) does not create data structure for
my.app.Address
, this data structure must already exist in your codebase and have all required
properties. If you don’t want to create this data structure manually, you can always
create SQL SELECT query that selects all required fields for Address
and use sharedResult
annotation to generate it automatically.
Check Collection Recipes for more details.
Key Annotations for perRow
-
dynamicField
: Name of the property in the result class -
mappingType=perRow
: Indicates flat row structure with nested objects -
propertyType
: Type of the nested object (my.app.Address
) -
sourceTable
: Table alias for the joined table (a
forPersonAddress
table) -
removeAliasPrefix
: Prefix to remove from column names (optional). In the example above,removeAliasPrefix=address_
will removeaddress_
prefix from all column name aliases, soaddress_id
will be mapped toid
property ofAddress
object. -
notNull=true
: Control nullability of the nested object (optional). With this you can make the nested object non-nullable, but use it only if you are sure that the joined table will always have a value for the selected rows.
Grouped Collection Mapping with ‘collection’
Use mappingType=collection
to group multiple joined records into collections within your result.
This returns one row per main entity with all related data grouped into collections.
Example: Person with Grouped Addresses
File: queries/person/selectWithGroupedAddresses.sql
-- @@{ collectionKey=person_id }
SELECT p.id AS person_id,
p.first_name,
p.last_name,
a.id AS address_id,
a.street,
a.city
/* @@{ dynamicField=addresses,
mappingType=collection,
propertyType=List<my.app.Address>,
sourceTable=a,
collectionKey=address_id
removeAliasPrefix=address_
notNull=true } */
FROM Person p
LEFT JOIN PersonAddress a ON p.id = a.person_id
This generates:
object PersonQuery {
object SelectWithGroupedAddresses {
data class Params(
val personId: Long
)
data class Result(
val personId: Long,
val firstName: String,
val lastName: String,
val addresses: List<Address> // Collection from JOIN
)
}
}
Result structure: If a person has 2 addresses, you get 1 result:
Row 1: person_1 | addresses = [aaddress_1, address_2]
The person data appears once with all addresses grouped into a collection.
Key Annotations for collection
- Statement-level
collectionKey
: Unique identifier for the main table (required for grouping) - Field-level
collectionKey
: Unique identifier for the collection items mappingType=collection
: Indicates grouped collection structurepropertyType
: Collection type (e.g.,List<Address>
)
Key Differences: perRow
vs collection
While in many cases perRow
is not as useful as collection
mapping, it can be useful in some
cases, for example when you deal with 1:1 relationships. Or when collection
mapping is not
flexible enough for what you need, and you want to perform more complex grouping, filtering
and other transformations.
Understanding when to use each mapping type:
perRow
- Flat Structure
- Use case: When you want each JOIN result as a separate row
- Result: Multiple rows, one per JOIN match
- Example: Person with 3 addresses = 3 result rows
- Good for: Pagination, filtering individual relationships, simple processing
collection
- Grouped Structure
- Use case: When you want related data grouped together
- Result: One row per main entity with nested collections
- Example: Person with 3 addresses = 1 result row with List<Address>
- Good for: Complete entity loading, reducing data transfer, complex domain models
Collection Key Formats
The collectionKey
annotation supports two formats:
1. Alias.Column Format
You can specify the collection key using the table alias and column name, separated by a dot.
/* @@{ collectionKey=id } */
SELECT p.id,
p.name,
a.id AS address_id
FROM Person p
LEFT JOIN Address a ON p.id = a.person_id
The id
referred to p.id
because collectionKey will be searching for id
in p
table.
2. Aliased Column Format
/* @@{ collectionKey=person_id } */
SELECT
p.id AS person_id,
p.name,
a.id AS address_id
FROM Person p
LEFT JOIN Address a ON p.id = a.person_id
Best Practices
- Use table aliases: Always use table aliases in SELECT statements for clarity
- Consistent naming: Use consistent naming patterns for collection keys
- Limit collections: Be mindful of performance with large collections
- NULL handling: Consider LEFT JOIN nullability in your domain logic
- Index optimization: Ensure proper indexes on JOIN columns for performance