Add JSON aggregate support for HasMany references#1286
Add JSON aggregate support for HasMany references#1286woodholly wants to merge 1 commit intoatk4:developfrom
Conversation
Features:
- JSON aggregate fields for HasMany references with 'aggregate' => 'json'
- Multiple expression types: strings, callables, Expressionable objects
- Dot notation for join field references (e.g., 'person.first_name')
- Custom field output names in aggregates
- Support for nested joins in JSON aggregates
- Database-specific implementations for all supported engines
Database support:
- MySQL: JSON_ARRAYAGG, json_object (5.7.8+)
- SQLite: json_group_array, json_object
- PostgreSQL: json_agg, json_build_object
- Oracle: JSON_ARRAYAGG, json_object
- MSSQL: json_array, json_object
API:
```php
$ref = $movie->hasMany('reviews', ['model' => [Review::class]]);
$ref->addField('reviews_json', [
'aggregate' => 'json',
'fields' => [
'rating',
'comment',
'reviewer'
],
]);
```
| "postgresql" | ||
| ], | ||
| "version": "dev-develop", | ||
| "version": "6.0.x-dev", |
| * Perform query operation on SQL server (such as select, insert, delete, etc). | ||
| * | ||
| * @method Expression fxJsonObject(array<string, Expressionable> $keyValuePairs) | ||
| * @method Expression jsonArrayAgg(Expressionable $expr) |
There was a problem hiding this comment.
This functionality has already #1268 PR but it is buggy on MySQL & MariaDB.
In MariaDB it is a confirmed bug.
In MySQL I did not investigate it in depth yet. Maybe it depends on configuration option.
The PR should be merged first and we should not integrate it before MySQL/MariaDB is either fixed or we can always throw an error if and only if the bug is applicable.
I see you are interested in HasMany aggregate = json and you have my support for it 👍.
To better understand your needs, what are your usecases for this PR?
There was a problem hiding this comment.
I see you are interested in HasMany aggregate = json and you have my support for it 👍.
Well, good low-code design should have nice, short methods! :)
By the way, this approach uses a bit of "magic" - the dot notation. What's your take on that? Personally, I'm not usually a fan of magic in libraries, but I'll make an exception for JSON aggregation. Most of the time, that JSON is used right after a join, so the convenience is worth it.
To better understand your needs, what are your usecases for this PR?
Example: movie database https://raw.githubusercontent.com/geldata/imdbench/master/docs/schema.png
Task: Get data for a movie page
- Fetch movie data by ID
- Include full cast list and full directors list
- Do this ASAP = in a single SQL query
This is difficult or impossible to achieve without JSON aggregation.
With this PR, I can accomplish this:
class Movie extends \App\Core\Model {
protected function init(): void {
...
$this->hasMany('directors', ['model' => function ($persistence) {
$model = new Directors($persistence);
$model->join('person');
return $model;
}, 'theirField' => 'movie_id'])
->addField('directors_json', [
'aggregate' => 'json',
'fields' => [
'person.id',
'full_name' => fn($q, $r) => $q->expr('CONCAT([], CASE WHEN [] != "" THEN CONCAT(" ", []) ELSE "" END, " ", [])', [$r('person.first_name '), $r('person.middle_name'), $r('person.middle_name'), $r('person.last_name')]),
'person.image',
],
'type' => 'json',
]);
$this->hasMany('cast', ['model' => function ($persistence) {
$model = new Cast($persistence);
$model->join('person');
return $model;
}, 'theirField' => 'movie_id'])
->addField('cast_json', [
'aggregate' => 'json',
'fields' => [
'person.id',
'full_name' => fn($q, $r) => $q->expr('CONCAT([], CASE WHEN [] != "" THEN CONCAT(" ", []) ELSE "" END, " ", [])', [$r('person.first_name '), $r('person.middle_name'), $r('person.middle_name'), $r('person.last_name')]),
'person.image',
],
'type' => 'json',
]);
...
}
$movie = new Movie($persistence);
$movie = $movie->load($id);
return json_encode([
'id' => $movie->get('id'),
...
'directors' => $movie->get('directors_json') ?? [],
'cast' => $movie->get('cast_json') ?? [],
]);This example clearly shows why we need both JSON aggregation and dot notation working together.
| } | ||
| } | ||
|
|
||
| $jsonObj = $query->fxJsonObject($jsonPairs); |
There was a problem hiding this comment.
This is legit usecase for a new method.
See #1267 for non-associative PR.
I would like to have this reviewed and merged separately.
Please reuse as much as code from #1267 as possible.
Like the non-object version, the object (associative) version should support MySQL 5.6, it should be possible to construct JSON string reliably without native JSON support.
| $author->hasMany('Books1', ['model' => $bookModel]) | ||
| ->addField('books_expr', [ | ||
| 'aggregate' => 'json', | ||
| 'fields' => ['info' => ['expr' => "CONCAT([], ' by ', [])", 'args' => ['title', 'author.name']]], |
There was a problem hiding this comment.
better to use Query::fxConcat() here, might also help you fixing the CI for all DB vendors
There was a problem hiding this comment.
Yes, but that test specifically validates this particular type of expression.
| $fx = function () use ($defaults, $field) { | ||
| return $this->refLink()->action('fx0', [$defaults['aggregate'], $field]); | ||
| }; | ||
| } elseif ($defaults['aggregate'] === 'json') { |
There was a problem hiding this comment.
json-array and json-object should be probably supported, as the first is much more storage effective and ordered (even on MySQL)
Features:
Database support:
API: