How do I generate nested json objects using mysql native json functions? - mysql

Using only the native JSON fuctions (no PHP, etc) in MySQL version 5.7.12 (section 13.16 in the manual) I am trying to write a query to generate a JSON document from relational tables that contains a sub object. Given the following example:
CREATE TABLE `parent_table` (
`id` int(11) NOT NULL,
`desc` varchar(20) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `child_table` (
`id` int(11) NOT NULL,
`parent_id` int(11) NOT NULL,
`desc` varchar(20) NOT NULL,
PRIMARY KEY (`id`,`parent_id`)
);
insert `parent_table` values (1,'parent row 1');
insert `child_table` values (1,1,'child row 1');
insert `child_table` values (2,1,'child row 2');
I am trying to generate a JSON document that looks like this:
[{
"id" : 1,
"desc" : "parent row 1",
"child_objects" : [{
"id" : 1,
"parent_id" : 1,
"desc" : "child row 1"
}, {
"id" : 2,
"parent_id" : 1,
"desc" : "child row 2"
}
]
}]
I am new to MySQL and suspect there is a SQL pattern for generating nested JSON objects from one to many relationships but I'm having trouble finding it.
In Microsoft SQL (which I'm more familiar with) the following works:
select
[p].[id]
,[p].[desc]
,(select * from [dbo].[child_table] where [parent_id] = [p].[id] for json auto) AS [child_objects]
from [dbo].[parent_table] [p]
for json path
I attempted to write the equivalent in MySQL as follows:
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',(select json_object('id',id,'parent_id',parent_id,'desc',`desc`)
from child_table where parent_id = p.id)
)
from parent_table p;
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',json_array((select json_object('id',id,'parent_id',parent_id,'desc',`desc`)
from child_table where parent_id = p.id))
)
from parent_table p
Both attempts fail with the following error:
Error Code: 1242. Subquery returns more than 1 row

The reason you are getting these errors is that the parent json object is not expecting a result set as one of its inputs, you need to have simple object pairs like {name, string} etc bug report - may be available in future functionality... this just means that you need to convert your multi row results into a concatination of results separated by commas and then converted into a json array.
You almost had it with your second example.
You can achieve what you are after with the GROUP_CONCAT function
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',json_array(
(select GROUP_CONCAT(
json_object('id',id,'parent_id',parent_id,'desc',`desc`)
)
from child_table
where parent_id = p.id))
)
from parent_table p;
This almost works, it ends up treating the subquery as a string which leaves the escape characters in there.
'{\"id\": 1,
\"desc\": \"parent row 1\",
\"child_objects\":
[\"
{\\\"id\\\": 1,
\\\"desc\\\": \\\"child row 1\\\",
\\\"parent_id\\\": 1
},
{\\\"id\\\": 2,
\\\"desc\\\": \\\"child row 2\\\",
\\\"parent_id\\\": 1}\"
]
}'
In order to get this working in an appropriate format, you need to change the way you create the JSON output as follows:
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',(select CAST(CONCAT('[',
GROUP_CONCAT(
JSON_OBJECT(
'id',id,'parent_id',parent_id,'desc',`desc`)),
']')
AS JSON) from child_table where parent_id = p.id)
) from parent_table p;
This will give you the exact result you require:
'{\"id\": 1,
\"desc\": \"parent row 1\",
\"child_objects\":
[{\"id\": 1,
\"desc\": \"child row 1\",
\"parent_id\": 1
},
{\"id\": 2,
\"desc\": \"child row 2\",
\"parent_id\": 1
}]
}'

For MariaDb, CAST AS JSON does not work. But JSON_EXTRACT may be used to convert a string to a JSON object:
select json_object(
'id',p.id
,'desc',p.`desc`
,'child_objects',JSON_EXTRACT(IFNULL((select
CONCAT('[',GROUP_CONCAT(
json_object('id',id,'parent_id',parent_id,'desc',`desc`)
),']')
from child_table where parent_id = p.id),'[]'),'$')
) from parent_table p;

I tried group_concat solution but I found it's problems larger string because of group_concat limitations (group_concat_max_len).
I wrote the new function resolve the problem about converting a string to JSON object as bellow and how to use it.
Tested on MariaDB 10.5.12
Usage: https://i.stack.imgur.com/cWfd7.jpg
CREATE FUNCTION `ut_tf_array`(input_json longtext) RETURNS longtext CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci
COMMENT 'Function for transform json array agg'
BEGIN
DECLARE transformed_data_list longtext ;
DECLARE record longtext ;
DECLARE i_count int ;
DECLARE i_count_items int ;
SET i_count = 0;
SET i_count_items = JSON_LENGTH(JSON_EXTRACT(input_json,'$'));
SET transformed_data_list = '[]';
-- return array with length = zero
IF input_json is NULL THEN
RETURN transformed_data_list;
END IF;
WHILE i_count < i_count_items DO
-- fetch into record
SELECT JSON_EXTRACT( JSON_EXTRACT( input_json ,'$') , CONCAT('$[',i_count,']')) INTO record;
-- append to transformed_data_list
SELECT JSON_ARRAY_APPEND(transformed_data_list, '$', JSON_EXTRACT(record, '$')) into transformed_data_list;
SET i_count := i_count + 1;
END WHILE;
-- done
RETURN transformed_data_list;
END

Below Query works for me.
SELECT JSON_ARRAYAGG(JSON_OBJECT('Id', p.id, 'desc', p.`desc`, 'child_objects', temp_json)) AS json_value
FROM (
SELECT p.id, p.`desc`,
JSON_ARRAYAGG(JSON_OBJECT('id', p.id, 'parent_id', p.parent_id, 'desc', p.`desc`)) AS temp_json
FROM parent_table p
GROUP BY p.id
) t;

Related

Sum fields inside json array in mysql

I have this table:
CREATE TABLE stackoverflow_question (
id int NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
json_ob mediumtext default null,
PRIMARY KEY (id)
);
I do some inserts:
insert into stackoverflow_question values(null, 'albert', '[{name: "albert1", qt: 2},{name: "albert2", qt: 2}]');
insert into stackoverflow_question values(null, 'barbara', '[{name: "barbara1", qt: 4},{name: "barbara2", qt: 7}]');
insert into stackoverflow_question values(null, 'paul', '[{name: "paul1", qt: 9},{name: "paul2", qt: 11}]');
Eventually, I will need to sort this table by total quantity.
in the examples above, "paul" has quantity = 20, while "barbara" has quantity = 11. And "albert" has quantity = 4.
Is it possible to create a select statement where a new field is created on the fly? Something like this:
SELECT
SUM (loop json_ob and sum all the quantity fields) AS total_quantity,
id,
name
FROM
stackoverflow_question
ORDER BY total_quantity
If json_ob is actually a valid json object then you can use JSON_TABLE() to extract the quantities and aggregate:
SELECT s.*, SUM(t.qt) total_quantity
FROM stackoverflow_question s,
JSON_TABLE(json_ob, '$[*]' COLUMNS (qt INTEGER PATH '$.qt')) t
GROUP BY s.id
ORDER BY total_quantity DESC;
See the demo.
According to jsonlint your JSON is not valid.
That's why this SQL returns an error (ERROR 3141 (22032): Invalid JSON text in argument 1 to function json_table: "Missing a name for object member." at position 2.")
SELECT
j.name, j.qt
FROM JSON_TABLE('[{name: "paul1", qt: 9},{name: "paul2", qt: 11}]',
"$[*]" COLUMNS (name varchar(20) PATH "$.name", qt int PATH "$.qt")) j ;
and this will return the values:
SELECT
j.name, j.qt
FROM JSON_TABLE('[{"name": "paul1", "qt": 9},{"name": "paul2", "qt": 11}]',
"$[*]" COLUMNS (name varchar(20) PATH "$.name", qt int PATH "$.qt")) j ;
output:
name
qt
paul1
9
paul2
11
You can convert your relaxedJSON, to JSON, using tools like : www.relaxedjson.org

How Split Json with oracle 19

Is there a way to take out part of a json?
I just want the common object without zona and central.
I have the following table
CREATE TABLE FB_TAB
( COL CLOB COLLATE USING_NLS_COMP,
ID NUMBER,
TYPE VARCHAR2(20 BYTE) COLLATE USING_NLS_COMP,
COLOR VARCHAR2(20 BYTE) COLLATE USING_NLS_COMP,
AMOUNT NUMBER,
APP VARCHAR2(20 BYTE) COLLATE USING_NLS_COMP,
CONSTRAINT JSON_CON_1 CHECK (col IS JSON) ENABLE
)
and its insert
insert into fb_tab
values('
{"common":{"contrato":{"id":"1","codigo":"054AKSDJ","nombre":"BUCLE"},"servicio":"","actividad":"Apertura","tipo_actividad":"BAJA","numero_administrativo":"","estado_origen":"Pendiente","provincia":{"id":"24","nombre":"León"},"aplicacion_origen":{"id":"1","nombre":"VISORD"},"zona":{"pais":"ES","cliente":"TL","div_geo":"2410002"},"central":{"codigo":"2410002","nombre":"Leon-Torre"},"clave":{"act_domiciliaria":"","prioridad":""}},"app_log":{"app_name":"client_mobile"}}
', 23, 'Ball', 'Red', 15, 'Mobile');
commit;
I want to get the next JSON as a result
{"Type":"Ball","Color":"Red","App":"Mobile","Amount":"15","my_json":{"contrato":{"id":"1","codigo":"054AKSDJ","nombre":"BUCLE"},"servicio":"","actividad":"Apertura","tipo_actividad":"BAJA","numero_administrativo":"","estado_origen":"Pendiente","provincia":{"id":"24","nombre":"León"},"aplicacion_origen":{"id":"1","nombre":"VISORD"},"clave":{"act_domiciliaria":"","prioridad":""}}}
I'm trying with this query
SELECT JSON_OBJECT (
'Type' value to_char(a.Type),
'Color' value to_char(a.Color),
'App' value to_char(a.App),
'Amount' value to_char(a.Amount),
'my_json' VALUE treat ( JSON_QUERY(a.col, '$.common' WITHOUT WRAPPER) as json )
)
--into json_output
FROM FB_TAB a
where a.id = :id;
but my actual result is this
{"Type":"Ball","Color":"Red","App":"Mobile","Amount":"15","my_json":{"contrato":{"id":"1","codigo":"054AKSDJ","nombre":"BUCLE"},"servicio":"","actividad":"Apertura","tipo_actividad":"BAJA","numero_administrativo":"","estado_origen":"Pendiente","provincia":{"id":"24","nombre":"León"},"aplicacion_origen":{"id":"1","nombre":"VISORD"},"zona":{"pais":"ES","cliente":"TL","div_geo":"2410002"},"central":{"codigo":"2410002","nombre":"Leon-Torre"},"clave":{"act_domiciliaria":"","prioridad":""}}}
I don't want to see zona and central
Is there a way to do this ?
Best regards
You have a mistake inside the JSON you're trying to insert into the table
Here's the revised insert statement
I've broke it down in pieces trying to fix it, feel free to "unwrap" it in a single line
insert into fb_tab
values('
{"common":
{"contrato":{"id":"1","codigo":"054AKSDJ","nombre":"BUCLE"},
"servicio":"",
"actividad":"Apertura",
"tipo_actividad":"BAJA",
"numero_administrativo":"",
"estado_origen":"Pendiente",
"provincia":{"id":"24","nombre":"León"},
"aplicacion_origen":{"id":"1","nombre":"VISORD"},
"clave":{"act_domiciliaria":"","prioridad":""}
},
"zona":{"pais":"ES","cliente":"TL","div_geo":"2410002"},
"central":{"codigo":"2410002","nombre":"Leon-Torre"},
"app_log":{"app_name":"client_mobile"}
}',
23, 'Ball', 'Red', 15, 'Mobile');
Basically you were placing clave after central and closing it with two curly brackets (}) making the common to include zona and central, that you were trying to exclude from the result of the query.
Now when you query the table
SELECT JSON_OBJECT (
'Type' value to_char(a.Type),
'Color' value to_char(a.Color),
'App' value to_char(a.App),
'Amount' value to_char(a.Amount)
,'my_json' VALUE treat ( JSON_QUERY(a.col, '$.common' WITHOUT WRAPPER) as json )
)
FROM FB_TAB a
where a.id = 23;
Removed the :id bind to ease the debugging process
You get the desired result
{"Type":"Ball","Color":"Red","App":"Mobile","Amount":"15","my_json":{"contrato":{"id":"1","codigo":"054AKSDJ","nombre":"BUCLE"},"servicio":"","actividad":"Apertura","tipo_actividad":"BAJA","numero_administrativo":"","estado_origen":"Pendiente","provincia":{"id":"24","nombre":"León"},"aplicacion_origen":{"id":"1","nombre":"VISORD"},"clave":{"act_domiciliaria":"","prioridad":""}}}

How can I update specific value from MySQL JSON column

I'm trying to update a specific value in JSON array from a MySQL column by searching for specific ID, I want it to be dynamic without specifying array key.
Here is what I tried:
update myTable JSON_REPLACE(`data`, $.OD, '3/1')
where JSON_EXTRACT(`data`, '$.ID') = 'some id';
Here is my column data:
[
{"ID":63010092,"IT":"OV80419947-63010092_1_3","OD":"21/10","SU":0,"OR":0},
{"ID":63010093,"IT":"OV80419947-63010093_1_3","OD":"8/5","SU":0,"OR":1},
{"ID":63010094,"IT":"OV80419947-63010094_1_3","OD":"8/5","SU":0,"OR":2}
]
Try (adjust everything you consider necessary):
UPDATE `myTable`
INNER JOIN JSON_TABLE(
`myTable`.`data`,
'$[*]'
COLUMNS(
`position` FOR ORDINALITY,
`ID` INT PATH '$.ID'
)
) `der`
SET `data` = JSON_REPLACE(
`myTable`.`data`,
CONCAT('$[', `der`.`position` - 1, '].OD'),
'3/1'
)
WHERE `der`.`ID` = 63010092;
See dbfiddle.

Data truncation: '' While Inserting or Updating Value

I am trying to Insert/update a string (JSON). While inserting/Updating the value I get data truncation error.
I have tried making a JSON type column and passing an JSON_OBJECT() type but that fails as well.
select '''[{"id":"1202","title":"Asian","notes":"","active":"1"}]''';
CREATE TABLE mktesttable (
id int NOT NULL,
s VARCHAR(34530) NOT NULL
);
INSERT INTO mktesttable
select 1, '''[{"id":"1202","title":"Asian","notes":"","active":"1"}]''';
select * from mktesttable;
// That Works
INSERT INTO mktesttable
SELECT
patient_data.id,
CONCAT(
'''[{"id":"', patient_data.race,
'","title":"', list_options.title,
'","notes":"', list_options.notes,
'","active":"', list_options.active,
'"}]'''
) as s
FROM
patient_data
INNER JOIN list_options
ON patient_data.race = list_options.id order by 1 desc
Yields same result (Id's and data varies) but doesn't work
Result Set
If you want to store JSON object, you should use the JSON datatype instead of strings. To create a JSON object, you can use JSON_OBJECT.
CREATE TABLE mktesttable (
id int NOT NULL,
s JSON NOT NULL
);
INSERT INTO mktesttable
SELECT
patient_data.id,
JSON_OBJECT(
'id', patient_data.race,
'title', list_options.title,
'notes', list_options.notes,
'active', list_options.active,
)
FROM
patient_data
INNER JOIN list_options
ON patient_data.race = list_options.id
ORDER BY patient_data.id desc
If you need a JSON array as shown in your sample data, then :
JSON_ARRAY(
JSON_OBJECT(
'id', patient_data.race,
'title', list_options.title,
'notes', list_options.notes,
'active', list_options.active,
)
)

How to Update a Specific Object in a JSON Array in Mysql 5.7

How can I update an object in an array based on a unique value in the object?
Let's say this is my json object stored in a table called objects and in a column called content
table: objects
id: 7383
content: { data:[{id: 111, active: 1 }, {id: 222, active: 1 }, {id: 333, active: 0 }] }
I can update objects if I know the position of the element in the array with
SET content = JSON_REPLACE(content,'$.data[1].active', 0)
Where id = 7383
However, if I don't know the position of the array, but I do know the value of id (for example 222) in the object, how can I update active to 0 for the object that has id: 222 ?
Currently, it's complicated to look up numerical values with MySQL JSON functions. In a JSON like the following, it would be simple:
{"id": "222", "active": 1}
There are many ways to get what you need, I present one that can give you ideas (modify everything that is necessary):
UPDATE `objects`
SET `objects`.`content` =
JSON_REPLACE(`objects`.`content`, CONCAT('$.data',
(SELECT
JSON_UNQUOTE(
REPLACE(
JSON_SEARCH(
REPLACE(
REPLACE(
REPLACE(
`der`.`content` ->> '$.data[*].id',
', ',
'","'),
']',
'"]'),
'[',
'["'),
'one',
'222'),
'$',
'')
)
FROM (SELECT `objects`.`content`
FROM `objects`
WHERE `objects`.`id` = 7383) `der`
), '.active'), 0)
WHERE `objects`.`id` = 7383;
Beware of possible performance problems.
See dbfiddle.
In the most recent version of MySQL (>= 8.0.4), the sentence would be much simpler:
UPDATE `objects`
INNER JOIN JSON_TABLE(
`objects`.`content`,
'$.data[*]' COLUMNS(
`rowid` FOR ORDINALITY,
`id` INT PATH '$.id'
)
) `der` ON `der`.`id` = 222
SET `objects`.`content` =
JSON_REPLACE(
`objects`.`content`,
CONCAT('$.data[', `der`.`rowid` - 1, '].active'),
0)
WHERE
`objects`.`id` = 7383;
See db-fiddle.
It can be achieved by combining the functions JSON_SEARCH, which returns a dirty json path to the item you need, and then, extract the value of the jsonpath with an array index, concatenate it with subpath we want to update and use JSON_SET to set a new value to the final json path (tested with MySQL 5.7.32):
-- INPUT ------------------------------------------------
-- unique value for an object in the array
SET #unique_value = "12345";
-- object field we want to update
SET #field_to_update = '.myField';
-- new value
SET #new_value = 1;
-- PROCESSING ------------------------------------------
-- Get json path to the item with specified #unique_value
-- RESULT: $.data[6].id
SET #temp_path = ( TRIM(BOTH '"' FROM ( SELECT JSON_SEARCH(json, 'one', #unique_value, NULL, "$.data")
FROM `my-table`
WHERE `column1` = "abcd" ) ));
-- We are looking for the bracket that delimits index within the array of documents: [11]
SET #closing_bracket_index = (SELECT LOCATE(']', #temp_path));
-- Get json path with index of an object for #unique_value
-- in MySQL, string indexing starts from position 1, not a zero
-- RESULT: $.data[6]
SET #item_path = ( SELECT SUBSTRING(#temp_path, 1, #closing_bracket_index) );
-- $.data[6].myFIeld
SET #item_path_to_update = ( SELECT CONCAT(#item_path, #field_to_update) );
-- UPDATE JSON STATEMENT
UPDATE `my-table`
SET json = JSON_SET(json-column, #item_path_to_update, #new_value)
WHERE `column1` = "abcd";