Insert n rows with LAST_INSERT_ID - mysql

I have 3 tables called POSTS, HASHTAGS and POSTS_HASHTAGS_RELATION, as below.
CREATE TABLE POSTS(
post_id int unsigned NOT NULL AUTO_INCREMENT,
content varchar(200) NOT NULL,
PRIMARY KEY (post_id)
);
CREATE TABLE HASHTAGS(
hashtag_id int unsigned NOT NULL AUTO_INCREMENT,
hashtag varchar(40) NOT NULL,
PRIMARY KEY (hashtag_id)
);
CREATE TABLE POSTS_HASHTAGS_RELATION(
post_id int unsigned NOT NULL,
hashtag_id int unsigned NOT NULL,
PRIMARY KEY (post_id, hashtag_id)
);
When user posts, they select upto 20 hashtags from those saved in HASHTAGS. I send the hashtag_id(s) of these hashtags from front end as comma separated string to backend where it is converted to list in nodejs.
Firstly, is there a better approach to struture this?
Secondly, how do I insert variable number of rows to POSTS_HASHTAGS_RELATION in a single query?
INSERT INTO POSTS (content) VALUES ('bla bla bla bla');
SET #post_id = LAST_INSERT_ID();
INSERT INTO POSTS_HASHTAGS_RELATION (post_id, hashtag_id) VALUES (#post_id, 19), (#post_id, 41) ...;
// Something like this but the number of values can be between 1 and 20
If this has been answered befored, just guide me to that answer. I am unable to find anything relevant. I assume this is not a very unique problem.

What you show is the way I would do it. You can insert multiple rows using INSERT in the manner you show, by writing multiple row constructors after the VALUES keyword. If you do, you must include a value for all the columns named in your INSERT statement in every row constructor. Therefore you must reference the #post_id variable in each row constructor.
If you really don't like to write #post_id more than once, you could do something like this:
INSERT INTO POSTS_HASHTAGS_RELATION (post_id, hashtag_id)
SELECT #post_id, hashtag_id FROM (
SELECT 19 AS hashtag_id UNION SELECT 41 UNION SELECT 42 UNION ...
) AS t;
But that seems less clear and readable than the way you were doing it.
Re your comment:
I'm not a node.js programmer, but I've used the technique in other languages to build an SQL statement with a number of row constructors based on the input list. Proper query parameters can only be used in place of scalar values, not lists or expressions or identifiers or SQL keywords, etc. But I understand node.js does some extra string-substitution magic, so they're not really doing query parameters.
Suppose you had just done your INSERT into POSTS, you could capture the last insert id:
var postId = result.insertId;
Then create a partial INSERT statement for your next insert:
insert = 'INSERT INTO POSTS_HASHTAGS_RELATION (post_id, hashtag_id) VALUES';
You will need an array for the row constructors and an array for the parameters:
let rowConstructors = [];
let parameters = [];
hashtags.forEach(function (hashtag) {
rowConstructors.push('(?, ?)');
parameters.concat(postId, hashtag);
});
Now you have an array of row constructors, which toString() will turn into a comma-separated string. And you have an array of values to pass as parameters.
connection.query(insert + rowConstructors.toString(),
parameters, function (error, results, fields) {
if (error) throw error;
// ...
});
I guess the .toString() is optional, because the array should be coerced to a string automatically by concatenating it to the insert string.
Again, I'm not a node.js programmer, so forgive me if there are errors or style problems. But that should give you the idea of this technique.

Saving the data into the tables in SP format:
CREATE PROCEDURE save_post_and_tags (post_content VARCHAR(200), hashtag_ids TEXT)
BEGIN
DECLARE post_id INT UNSIGNED;
DECLARE tag_id INT UNSIGNED;
INSERT INTO posts VALUES (DEFAULT, post_content);
SET post_id := LAST_INSERT_ID();
SET hashtag_ids := concat(hashtag_ids, ',');
REPEAT
SET tag_id := 0 + SUBSTRING_INDEX(hashtag_ids, ',', 1);
SET hashtag_ids := TRIM(SUBSTRING(hashtag_ids FROM 1 + LOCATE(',', hashtag_ids)));
INSERT INTO posts_hashtags_relation VALUES (post_id, tag_id);
UNTIL hashtag_ids = '' END REPEAT;
END
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=a9c622ea7ad2dd48dba18312c7a33487 (sloppy CSV is used on purpose).

Related

How do I insert data into a table (and return it) when a select statement outputs no rows?

I have a MySql table that is created like this;
CREATE TABLE test_contacts
(
id int PRIMARY KEY AUTO_INCREMENT,
name varchar(16) NOT NULL,
databit int NOT NULL
);
CREATE UNIQUE INDEX test_contacts_name_uindex ON test_contacts (name);
When I want to retrieve data i do SELECT * FROM test_contacts WHERE name = '{name}';
In my current java application I am doing the following: (pseudocode)
Object result = SELECT * FROM test_contacts WHERE name = '{name}';
if (result == null) {
INSERT INTO test_contacts (`name`, `databit`) VALUES ('{name}', 2);
result = SELECT * FROM test_contacts WHERE name = '{name}';
}
Is there a way to compact these 3 database calls into 1 statement that always returns a row for the specified name? (I need the id that is inserted)
Since you have a unique index anyway, instead of checking in your code, if your first select had any result, you simply do
INSERT IGNORE test_contacts (`name`, `databit`) VALUES ('{name}', 2);
read more about it here
Then you get the id with
SELECT LAST_INSERT_ID();
read more about it here

Table Valued Function is returning only one row when the actual result contains multiple rows

I have table Clients that contains many columns, however I'm targeting two particular columns ClientId and PIN. The example that I'm going to provide is not making much sense but I'm just learning more advanced SQL (T-SQL) topics and my final goal is something a lot different than what is here.
So I made a function which I would like to return all rows that match certain search criteria. So I have this:
CREATE FUNCTION ufn_NumberOfClients (#ClientId INT)
RETURNS #retContactInformation TABLE
(
-- Columns returned by the function
ClId int PRIMARY KEY NOT NULL,
PIN nvarchar(50) NULL
)
AS
BEGIN
DECLARE
#ClId INT,
#PIN nvarchar(50)
SELECT #ClId = ClientId, #PIN = PIN
FROM Client
WHERE PIN LIKE '%7788%'
IF #ClientId IS NOT NULL
BEGIN
INSERT #retContactInformation
SELECT #ClId, #PIN;
END;
RETURN;
END
But when I execute the function:
SELECT * FROM
ufn_NumberOfClients(4)
I get only one result. Even though it's a valid result, if I execute just
SELECT * FROM Client
WHERE PIN LIKE '%7788%'
I get 5 results, and the function is returning just the last one.
Two thing which worth mentioning. The example which I use to write the function the ClId int PRIMARY KEY NOT NULL, from the table is not DECLAREed after that, but I got error if I don't do it. Also, I pass variable, but I think it's not used so it shouldn't affect the behaviour (or at least I think so).
So how can I return all the results matching the search crieria with a function?
You actually insert only one row in the table variable right now, Try this instead :
CREATE FUNCTION ufn_NumberOfClients (#ClientId INT)
RETURNS #retContactInformation TABLE
(
-- Columns returned by the function
ClId int PRIMARY KEY NOT NULL,
PIN nvarchar(50) NULL
)
AS
BEGIN
INSERT INTO #retContactInformation
SELECT ClientId, PIN
FROM Client
WHERE PIN LIKE '%7788%';
RETURN;
END

Conditional insert based on LAST_INSERT_ID

A nearly identical question was asked here using an IF statement, but he didn't get an actionable answer, just suggested to go here where no IF statements are being used. I've tried to write both an IF statement and a conditional statement using the latter link but I'm stuck (see below).
I want to be able to conditionally insert a row only if the previous insert attempt actually inserted a row (ROW_COUNT > 0). The previous insert could have been duplicate data, so I'm deliberately setting it's LAST_INSERT_ID to null so no subsequent child inserts can occur with that LAST_INSERT_ID. The SQL script is created by a C# script, so it would be very possible that the LAST_INSERT_ID is not pointing to where you'd expect.
Here's a very small example of the script generated code (there are ~3 million rows in the final database):
SET #Vendors_Vendor_ID = (SELECT vendor_ID FROM VENDORS WHERE vendorName = 'PCA');
INSERT IGNORE INTO PCBID (PCBID, PCBDrawing, AssemblyDrawing, PONumber, Vendors_Vendor_ID)
VALUES (11001, '10405', '41606', '091557.5', #Vendors_Vendor_ID);
SET #eventType_ID = (SELECT EVENTTYPE_ID FROM EVENTTYPES WHERE EVENTTYPE = 'Creation');
SET #USER = 'CTHOMAS';
INSERT IGNORE INTO EVENTS (PCBID, EVENTTYPE_ID, DATETIME, USER)
VALUES (11001, #eventType_ID, '2009-06-15T13:15:27', #USER);
SET #EVENT_ID = IF(ROW_COUNT() > 0, LAST_INSERT_ID(), null);
-- THIS DOES NOT WORK
SELECT IF(#EVENT_ID != null,
INSERT INTO EVENTDETAILS (EVENT_ID, ITEMNAME, ITEMVALUE)
VALUES (#EVENT_ID, 'Notes', 'WO#89574'),
null);
-- THIS DOESN'T WORK EITHER
INSERT INTO EVENTDETAILS (EVENT_ID, ITEMNAME, ITEMVALUE)
SELECT #EVENT_ID, 'Notes', 'WO#89574'
WHERE #EVENT_ID != null;
The PCBID table is not a problem for duplicate data, and the Events table has a composite unique key which prevents duplicate data by using INSERT IGNORE:
CREATE TABLE IF NOT EXISTS `uniqueTest`.`events` (
`Event_ID` INT(11) NOT NULL AUTO_INCREMENT ,
`PCBID` INT(11) NOT NULL ,
`EventType_ID` INT(11) NOT NULL ,
`DateTime` DATETIME NOT NULL ,
`User` VARCHAR(45) NOT NULL ,
PRIMARY KEY (`Event_ID`) ,
UNIQUE KEY `PDU_Index` (`PCBID`, `DateTime`, `User`),
The Problem:
I need to be able to do a conditional insert based on the previous insert attempt into the Events table, if it was ignored (because it's duplicate data), don't insert any child rows either. There's currently no way to make any of the EventDetail data unique, there could be multiple rows of legitimate data based on a given Event_ID.
There are four levels deeper possible below the Events table depending on what type of data it is, if the event data doesn't get inserted because it's duplicate data, no child data gets written either (because it'll be duplicate as well).
Your second try was nearly right. You've got to check of NULL values with IS NOT NULL. So use
INSERT INTO EVENTDETAILS (EVENT_ID, ITEMNAME, ITEMVALUE)
SELECT #EVENT_ID, 'Notes', 'WO#89574' FROM DUAL
WHERE #EVENT_ID IS NOT NULL; -- instead of !=
or
INSERT INTO EVENTDETAILS (EVENT_ID, ITEMNAME, ITEMVALUE)
SELECT t.* FROM (
SELECT #EVENT_ID, 'Notes', 'WO#89574'
) t
WHERE #EVENT_ID IS NOT NULL; -- instead of !=
The first one cannot work:
-- THIS DOES NOT WORK
SELECT IF(#EVENT_ID != null,
INSERT INTO EVENTDETAILS (EVENT_ID, ITEMNAME, ITEMVALUE) ...
because the syntax of IF is
IF(expr1,expr2,expr3)
If expr1 is TRUE (expr1 <> 0 and expr1 <> NULL) then IF() returns expr2; otherwise it returns expr3. IF() returns a numeric or string value, depending on the context in which it is used.
Conditional execution of statements is only possible in stored routines. The IF syntax of stored routines would allow something like
IF #EVENT_ID IS NOT NULL THEN
INSERT INTO EVENTDETAILS (EVENT_ID, ITEMNAME, ITEMVALUE) ...
END IF
You've got to distinguish those both syntax versions.

PostgreSQL: insert data into table from json

Now I use to manually parse json into insert string like so
insert into Table (field1, field2) values (val1, val2)
but its not comfortable way to insert data from json!
I've found function json_populate_record and tried to use it:
create table test (id serial, name varchar(50));
insert into test select * from json_populate_record(NULL::test, '{"name": "John"}');
but it fails with the message: null value in column "id" violates not-null constraint
PG knows that id is serial but pretends to be a fool. Same it do for all fieds with defaults.
Is there more elegant vay to insert data from json into a table?
There's no easy way for json_populate_record to return a marker that means "generate this value".
PostgreSQL does not allow you to insert NULL to specify that a value should be generated. If you ask for NULL Pg expects to mean NULL and doesn't want to second-guess you. Additionally it's perfectly OK to have a generated column that has no NOT NULL constraint, in which case it's perfectly fine to insert NULL into it.
If you want to have PostgreSQL use the table default for a value there are two ways to do this:
Omit that row from the INSERT column-list; or
Explicitly write DEFAULT, which is only valid in a VALUES expression
Since you can't use VALUES(DEFAULT, ...) here, your only option is to omit the column from the INSERT column-list:
regress=# create table test (id serial primary key, name varchar(50));
CREATE TABLE
regress=# insert into test(name) select name from json_populate_record(NULL::test, '{"name": "John"}');
INSERT 0 1
Yes, this means you must list the columns. Twice, in fact, once in the SELECT list and once in the INSERT column-list.
To avoid the need for that this PostgreSQL would need to have a way of specifying DEFAULT as a value for a record, so json_populate_record could return DEFAULT instead of NULL for columns that aren't defined. That might not be what you intended for all columns and would lead to the question of how DEFAULT would be treated when json_populate_record was not being used in an INSERT expression.
So I guess json_populate_record might be less useful than you hoped for rows with generated keys.
Continuing from Craig's answer, you probably need to write some sort of stored procedure to perform the necessary dynamic SQL, like as follows:
CREATE OR REPLACE FUNCTION jsoninsert(relname text, reljson text)
RETURNS record AS
$BODY$DECLARE
ret RECORD;
inputstring text;
BEGIN
SELECT string_agg(quote_ident(key),',') INTO inputstring
FROM json_object_keys(reljson::json) AS X (key);
EXECUTE 'INSERT INTO '|| quote_ident(relname)
|| '(' || inputstring || ') SELECT ' || inputstring
|| ' FROM json_populate_record( NULL::' || quote_ident(relname) || ' , json_in($1)) RETURNING *'
INTO ret USING reljson::cstring;
RETURN ret;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Which you'd then call with
SELECT jsoninsert('test', '{"name": "John"}');

mysql insert if not exists on joined tables

My tables look like this:
Table "posts"
posts.id = PK, auto-increment
posts.text = varchar
posts.tag_id 1 = int, FK to tags.id
posts.tag_id 2 = int, FK to tags.id
posts.tag_id 3 = int, FK to tags.id
Table "tags"
tags.id = PK, auto-increment
tags.tag = varchar
Now I want to insert the following data:
text: 'lorem ipsum'
tag1: 'music'
tag2: 'pop'
tag3: 'singer'
So I need a query that checks if tag1/tag2/tag3 already exist in "tags", inserts them otherwise and then insert those as foreign-keys + the "text" into a new row in "posts".
I had a look into mysql INSERT IF NOT EXISTS but I'm just stuck and don't know where to start. I know I could handle it with multiple queries but there has to be another, lighter way to achieve the same result.
Has anyone any experience with this?
Update
A friend of mine proposed something like this:
CREATE FUNCTION getTagID(tag VARCHAR('100')) RETURNS int
INSERT INTO posts (text,tag1,tag2,tag3)
VALUES ('lorem ipsum', getTagID('music'), getTagID('pop'), getTagID('singer'));
Of course the implementation of getTagId is still missing, but does that make sense? getTagID should select the id with the given tag, if it doesn't exist, insert it and return this. Any help is appreciated.
Solution
I created a custom function in MySql:
DELIMITER ;;
CREATE FUNCTION getTagID(tag VARCHAR(100)) RETURNS BIGINT
BEGIN
DECLARE tagID BIGINT;
SET tagID = (SELECT id FROM tags WHERE text = tag);
IF tagID IS NULL
THEN
INSERT INTO tags (text) VALUES (tag);
SET tagID = (SELECT LAST_INSERT_ID());
END IF;
RETURN tagID;
END;;
DELIMITER ;
and now I can just insert into posts like that:
INSERT INTO posts (text,tag1,tag2,tag3)
VALUES ('lorem ipsum', getTagID('music'), getTagID('pop'), getTagID('singer'));
with that function, which inserts into "tags" only if the tag does not yet exist and gives back the ID of the existing or newly created tag. Yipeee :)
You have to insert to posts, then insert to tags. There is no multi-table insert solution in SQL.
You can't even insert the post text to posts from a trigger on tags, because the insert to tags can only carry columns that belong to tags.
You can use INSERT IGNORE or REPLACE or INSERT ... ON DUPLICATE KEY UPDATE if you need to avoid duplicates in the tags table. See my answer to "INSERT IGNORE" vs "INSERT ... ON DUPLICATE KEY UPDATE" for more details on that.
Re your comment.
You can get the generated PK only if you let the auto-increment mechanism generate a new id. If you let auto-increment happen, you're guaranteed not to result in a duplicate PK.
If you specify an id and bypass auto-increment, you can't get the id anyway. But you don't need to query for the value if you specified it in your INSERT -- you should have it already.
The only other case is if you have a secondary unique key that's not auto-increment.
CREATE TABLE foo (id int auto_increment primary key, name varchar(10), unique key (name));
INSERT INTO foo (name) VALUES ('bill');
SELECT LAST_INSERT_ID(); -- returns 1
INSERT INTO foo (name) VALUES ('bill');
SELECT LAST_INSERT_ID(); -- returns 1
This is slightly confusing because the last insert id comes from the last INSERT that succeeded and generated an id.
Re your update with INSERT INTO posts (text,tag1,tag2,tag3). That's called repeating groups. What if you need four tags? What if a post has only two tags? How do you make an indexed search for posts with a given tag?