Can we have sth like this in MariaDB?
ALTER TABLE `files` ADD
CONSTRAINT `fk_persons_cover`
FOREIGN KEY (`foreign_key`, `model`)
REFERENCES `persons` (`uuid`, "persons_cover")
ON DELETE NO ACTION
ON UPDATE NO ACTION;
I want refrence from files to persons table when files.uuid=persons.uuid and files.model="persons_cover"
Now with this code MariaDB said:
#1064 - You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"persons_cover")
ON DELETE NO ACTION
ON UPDATE NO ACTION' at line 4
Is there a solution?
Edit:
I know can use filelable super/interface table like this solution
Or use some auto generated fields in files table like this solution
but they ar not good solution I think.
At first solution we can't find file 1 (row 1) is associated to what? or can't find list of persons 1 (person row with pk = 1) files
At second solution we should add nullable foreign key field per new association!
It should work, if the PERSONS table has a composite primary key (or unique key), encompassing uuid and persons_cover eg
create table persons(
uuid varchar( 100 )
, persons_cover varchar( 100 )
, constraint persons_pkey primary key ( uuid, persons_cover )
) ;
create table files (
uuid varchar( 100 )
, model varchar( 100 )
) ;
alter table files
add constraint fk_persons_cover
foreign key( uuid, model ) references persons( uuid, persons_cover ) ;
If, in your PERSONS table, (only) the UUID column is the primary key column, the composite foreign key may not work. However, in this case, you could add a composite UNIQUE key to the PERSONS table (uuid and person_cover). Should the values stored in the table allow this, then you can add the composite foreign key to FILES.
create table persons2(
uuid varchar( 100 )
, persons_cover varchar( 100 )
, constraint persons_pkey primary key ( uuid ) -- PK: only one column
) ;
-- does not work
alter table files
add constraint fk_persons_cover2
foreign key( uuid, model ) references persons2( uuid, persons_cover ) ;
-- error
-- Failed to add the foreign key constraint. Missing index for constraint 'fk_persons_cover2' in the referenced table 'persons2'
-- add a composite unique key
alter table persons2
add constraint upc_unique unique( uuid, persons_cover ) ;
-- no problem
alter table files
add constraint fk_persons_cover2
foreign key( uuid, model ) references persons2( uuid, persons_cover )
on delete no action
on update no action
;
DBfiddle here.
REFERENCES `persons` (`uuid`, "persons_cover")
No, you can't put a string literal in this line. You must name only column names that exist in the persons table. Typically these are the columns of the primary key of that table.
The foreign key constraint applies to all rows of your table. There's no such thing as a conditional foreign key that only applies on some rows.
I understand you think the workarounds you linked to are not good solutions, but that should be a clue that polymorphic associations are not a good design.
I wrote more about the disadvantages of polymorphic associations in numerous of my answers on Stack Overflow, and in a chapter of my book SQL Antipatterns, Volume 1:
Avoiding the Pitfalls of Database Programming.
Related
Is there a way to get a list of tables that have one-on-one relationship to a given table in SQLite3?
For example, here table ab has a one-on-one relationship with both table abc and abd. Is there a query or queries to return abc and abd for the given table name ab?
-- By default foreign key is diabled in SQLite3
PRAGMA foreign_keys = ON;
CREATE TABLE a (
aid INTEGER PRIMARY KEY
);
CREATE TABLE b (
bid INTEGER PRIMARY KEY
);
CREATE TABLE ab (
aid INTEGER,
bid INTEGER,
PRIMARY KEY (aid, bid)
FOREIGN KEY (aid) REFERENCES a(aid)
FOREIGN KEY (bid) REFERENCES b(bid)
);
-- tables 'ab' and 'abc' have a one-on-one relationship
CREATE TABLE abc (
aid INTEGER,
bid INTEGER,
name TEXT NOT NULL,
PRIMARY KEY (aid, bid) FOREIGN KEY (aid, bid) REFERENCES ab(aid, bid)
);
-- tables 'ab' and 'abd' have a one-on-one relationship
CREATE TABLE abd (
aid INTEGER,
bid INTEGER,
value INTEGER CHECK( value > 0 ),
PRIMARY KEY (aid, bid) FOREIGN KEY (aid, bid) REFERENCES ab(aid, bid)
);
CREATE TABLE w (
id INTEGER PRIMARY KEY
);
The following tedious precedure may get me the list of tables I want:
Get primary keys for table ab:
SELECT l.name FROM pragma_table_info('ab') as l WHERE l.pk > 0;
get foreign keys for other tables (this case is for table abd):
SELECT * from pragma_foreign_key_list('abd');
Do parsing to get what the list of tables of one-on-one relationships.
However, there must exist a more elegant way, I hope.
For SQL Server, there are sys.foreign_keys and referenced_object_id avaible (see post). Maybe there is something similar to that in SQLite?
Edit: adding two more tables for test
-- tables 'ab' and 'abe' have a one-on-one relationship
CREATE TABLE abe (
aid INTEGER,
bid INTEGER,
value INTEGER CHECK( value < 0 ),
PRIMARY KEY (aid, bid) FOREIGN KEY (aid, bid) REFERENCES ab
);
-- tables 'ab' and 'abf' have a one-on-one relationship
CREATE TABLE abf (
aidQ INTEGER,
bidQ INTEGER,
value INTEGER,
PRIMARY KEY (aidQ, bidQ) FOREIGN KEY (aidQ, bidQ) REFERENCES ab(aid, bid)
);
Edit: verify FK for table abe
sqlite> PRAGMA foreign_keys;
1
sqlite> .schema abe
CREATE TABLE abe (
aid INTEGER,
bid INTEGER,
value INTEGER CHECK( value < 0 ),
PRIMARY KEY (aid, bid) FOREIGN KEY (aid, bid) REFERENCES ab
);
sqlite> DELETE FROM abe;
sqlite> INSERT INTO abe (aid, bid, value) VALUES (2, 1, -21);
sqlite> INSERT INTO abe (aid, bid, value) VALUES (-2, 1, -21);
Error: FOREIGN KEY constraint failed
sqlite> SELECT * FROM ab;
1|1
1|2
2|1
Alternative
Although not a single query solution the following only requires submission/execution of a series of queries and is therefore platform independent.
It revolves around using two tables:-
a working copy of sqlite_master
a working table to store the the output of SELECT pragma_foreign_key_list(?)
Both tables are created via a CREATE-SELECT, although neither has any rows copied, so the tables are empty.
A trigger is applied to the working copy of sqlite_master to insert into the table that stores the result of SELECT pragma_foreign_key_list(table_name_from_insert);
The relevant rows are copied from sqlite_master via a SELECT INSERT and thus the triggering populates the store table.
The following is the testing code :-
DROP TABLE IF EXISTS fklist;
DROP TABLE IF EXISTS master_copy;
DROP TRIGGER IF EXISTS load_fklist;
/* Working version of foreign_key_list to store ALL results of SELECT pragma_foreign_key_list invocation */
CREATE TABLE IF NOT EXISTS fklist AS SELECT '' AS child,*
FROM pragma_foreign_key_list((SELECT name FROM sqlite_master WHERE type = 'not a type' LIMIT 1));
/* Working version of sqlite master */
CREATE TABLE IF NOT EXISTS master_copy AS SELECT * FROM sqlite_master WHERE type = 'not a type';
/* Add an after insert trigger for master copy to add to fklist */
CREATE TRIGGER IF NOT EXISTS load_fklist
AFTER INSERT ON master_copy
BEGIN
INSERT INTO fklist SELECT new.name,* FROM pragma_foreign_key_list(new.name);
END
;
/* Populate master_copy from sqlite_master (relevant rows)
and thus build the fklist
*/
INSERT INTO master_copy SELECT *
FROM sqlite_master
WHERE type = 'table'
AND instr(sql,' REFERENCES ') > 0
;
SELECT * FROM fklist;
DROP TABLE IF EXISTS fklist;
DROP TABLE IF EXISTS master_copy;
DROP TRIGGER IF EXISTS load_fklist;
Using a similar test base as per the previous answer the above results in :-
Is there a way to get a list of tables that have one-on-one relationship to a given table in SQLite3?
Not with certainty as coding a Foreign Key constraint does not define a relationship (rather it supports a relationship), that is relationships can exists without a FK constraint.
A Foreign Key constraint defines:-
a) a rule that enforces referential integrity
b) optionally maintains/alters referential integrity when the referred to column is changed (ON DELETE and ON UPDATE )
As such looking at the Foreign Key List only tells you where/if a FK constraint has been coded.
Saying that the following will get the tables with the constraint and the referenced tables.
More elegant is a matter of opinion, so it's up to you :-
WITH cte_part(name,reqd,rest) AS (
SELECT name,'',substr(sql,instr(sql,' REFERENCES ') + 12)||' REFERENCES '
FROM sqlite_master
WHERE sql LIKE '% REFERENCES %(%'
UNION ALL
SELECT
name,
substr(rest,0,instr(rest,' REFERENCES ')),
substr(rest,instr(rest,' REFERENCES ') + 12)
FROM cte_part
WHERE length(rest) > 12
)
SELECT DISTINCT
CASE
WHEN length(reqd) < 1 THEN name
ELSE
CASE substr(reqd,1,1)
WHEN '''' THEN substr(replace(reqd,substr(reqd,1,1),''),1,instr(reqd,'(')-3)
WHEN '[' THEN substr(replace(replace(reqd,'[',''),']',''),1,instr(reqd,'(')-3)
WHEN '`' THEN substr(replace(reqd,substr(reqd,1,1),''),1,instr(reqd,'(')-3)
ELSE substr(reqd,1,instr(reqd,'(')-1)
END
END AS tablename
FROM cte_part
;
As an example of it's use/results :-
screenshot from Navicat
Here's an adaptation of the above that includes, where appropriate, the child table that references the parent :-
WITH cte_part(name,reqd,rest) AS (
SELECT name,'',substr(sql,instr(sql,' REFERENCES ') + 12)||' REFERENCES '
FROM sqlite_master
WHERE sql LIKE '% REFERENCES %(%'
UNION ALL
SELECT
name,
substr(rest,0,instr(rest,' REFERENCES ')),
substr(rest,instr(rest,' REFERENCES ') + 12)
FROM cte_part
WHERE length(rest) > 12
)
SELECT DISTINCT
CASE
WHEN length(reqd) < 1 THEN name
ELSE
CASE substr(reqd,1,1)
WHEN '''' THEN substr(replace(reqd,substr(reqd,1,1),''),1,instr(reqd,'(')-3)
WHEN '[' THEN substr(replace(replace(reqd,'[',''),']',''),1,instr(reqd,'(')-3)
WHEN '`' THEN substr(replace(reqd,substr(reqd,1,1),''),1,instr(reqd,'(')-3)
ELSE substr(reqd,1,instr(reqd,'(')-1)
END
END AS tablename,
CASE WHEN length(reqd) < 1 THEN '' ELSE name END AS referrer
FROM cte_part
;
Example of the Result :-
the artists table is referenced by albums as the SQL used to create the albums table is CREATE TABLE 'albums'([AlbumId] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL ,[Title] TEXT NOT NULL ,[ArtistId] INTEGER NOT NULL , FOREIGN KEY ([ArtistId]) REFERENCES 'artists'([ArtistId]))
i.e. FOREIGN KEY ([ArtistId]) REFERENCES 'artists'([ArtistId]))
the employees table is self-referencing as per CREATE TABLE 'employees'(.... REFERENCES 'employees'([EmployeeId]))
Additional re comment:-
(I am still trying to understand your code...)
The code is based upon selecting rows from sqlite_master where the row is for a table (type = 'table'), as opposed to an index, trigger or view and where the sql column contains the word REFERENCES with a space before and after and there is a following left parenthesis.
The last condition used to weed out the likes of CREATE TABLE oops (`REFERENCES` TEXT, `x REFERENCES Y`);
For each selected row 3 columns are output:-
name which is the name of the table as extracted from the name column of sqlite_master,
reqd is initially an empty string (i.e. initial)
rest the rest of sql that follows the referred to table name with suffixed with REFERENCES.
The UNION ALL adds rows that are built upon what is newly added to the CTE, i.e. the three columns are extracted as per :-
name is the name
reqd is the sql from the rest column up until the first REFERENCES term (i.e. the table and referenced column(s))
rest is the sql from after the REFERENCES term
As with any recursion the end needs to be detected, this is when the entire sql statement has been reduced to being less than 12 (i.e the length of " REFERENCES ", the term used for splitting the sql statement).
This is what is termed as a RECURSIVE CTE
Finally the resultant CTE is then queried. If the reqd field is empty then the tablename column is the name column otherwise (i.e. the reqd column contains data(part of the sql)) the table name is extracted (part up to left parenthesis if not enclosed (`,' or [ with ])) or extracted from between the enclosure.
The following is what the final query results in if all the CTE columns are included (some data has been truncated):-
As can clearly be seen the extracted sql progressively reduces
The answer is intended as in-principle and has not been extensively tested to consider all scenarios, it may well need tailoring.
I tried to re-engineer a database, that I use at work. The one at work is MS Access. At home, it's MariaDB. For convenience, I use MySQL Workbench.
When sending the complete SQL dump to the server, I get an error concerning some foreign key not being correctly formed. I guess, it is a minor mistake, but still I cannot find it.
My InnoDB status tells me this:
LATEST FOREIGN KEY ERROR
2018-10-03 00:18:29 409c7450 Error in foreign key constraint of table `mydb`.`IF`:
FOREIGN KEY (`belegid`)
REFERENCES `mydb`.`tblBelegPositionen` (`belegfID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_tblBelege_tblECKassenschnittPositionen10`
FOREIGN KEY (`belegid`)
REFERENCES `mydb`.`tblECKassenschnittPositionen` (`belegfID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB:
Cannot find an index in the referenced table where the
referenced columns appear as the first columns, or column types
in the table and the referenced table do not match for constraint.
Note that the internal storage type of ENUM and SET changed in
tables created with >= InnoDB-4.1.12, and such columns in old tables
cannot be referenced by such columns in new tables.
See http://dev.mysql.com/doc/refman/5.6/en/innodb-foreign-key-constraints.html
for correct foreign key definition.
Create table '`mydb`.`IF`' with foreign key constraint failed. There is no index in the referenced table where the referenced columns appear as the first columns near '
FOREIGN KEY (`belegid`)
REFERENCES `mydb`.`tblBelegPositionen` (`belegfID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_tblBelege_tblECKassenschnittPositionen10`
FOREIGN KEY (`belegid`)
REFERENCES `mydb`.`tblECKassenschnittPositionen` (`belegfID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB'.
The really weird thing is that I do not have any table named "IF"...
Can anyone make heads or tails of this for me? That would be very much appreciated.
-- Table `mydb`.`tblBelegPositionen`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `mydb`.`tblBelegPositionen` ;
SHOW WARNINGS;
CREATE TABLE IF NOT EXISTS `mydb`.`tblBelegPositionen` (
`belegposid` INT NOT NULL AUTO_INCREMENT,
`belegposBetrag` DOUBLE NOT NULL,
`zahlartfID` INT NOT NULL,
`belegfID` INT NOT NULL,
PRIMARY KEY (`belegposid`))
ENGINE = InnoDB;
SHOW WARNINGS;
-- Table `mydb`.`tblECKassenschnittPositionen`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `mydb`.`tblECKassenschnittPositionen` ;
SHOW WARNINGS;
CREATE TABLE IF NOT EXISTS `mydb`.`tblECKassenschnittPositionen` (
`ecposid` INT NOT NULL AUTO_INCREMENT,
`belegfID` INT NOT NULL,
`ecposBetrag` DOUBLE NOT NULL,
`kassenschnittfID` INT NOT NULL,
PRIMARY KEY (`ecposid`))
ENGINE = InnoDB;
SHOW WARNINGS;
-- Table `mydb`.`tblBelege`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `mydb`.`tblBelege` ;
SHOW WARNINGS;
CREATE TABLE IF NOT EXISTS `mydb`.`tblBelege` (
`belegid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`belegKassierer` INT NOT NULL,
`belegDatum` DATETIME NOT NULL,
`kassefID` INT NOT NULL,
`belegSchicht` INT NULL,
`gvfID` INT NOT NULL,
`belegJahr` YEAR NULL,
`belegDruckErfolgt` TINYINT(1) NULL,
`belegDruckDatum` DATETIME NULL,
`belegPeriodenfremdeBuchung` TINYINT(1) NULL,
PRIMARY KEY (`belegid`, `gvfID`, `kassefID`),
CONSTRAINT `fk_tblBelege_tblBelegPositionen10`
FOREIGN KEY (`belegid`)
REFERENCES `mydb`.`tblBelegPositionen` (`belegfID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_tblBelege_tblECKassenschnittPositionen10`
FOREIGN KEY (`belegid`)
REFERENCES `mydb`.`tblECKassenschnittPositionen` (`belegfID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
SHOW WARNINGS;
Ok, so there are a couple of things to know here, as there were a couple of errors. You must check all the items that the error mentions, for correctness, to avoid trouble.
The cannot find an index in the referenced table portion of the error message means that the column/field specified in the REFERENCES clause must be indexed in the other table.
Then, the column type definition of the column specified in the FOREIGN KEY clause must match the column type of the column specified in the REFERENCES clause too, so even though the first item corrects part of the problem, there will still be an error related to another portion of the message: ...or column types in the table and the referenced table do not match.
So, to fix item 1, run these 2 queries:
ALTER TABLE `tblbelegpositionen` ADD INDEX(`belegfID`);
ALTER TABLE `tbleckassenschnittpositionen` ADD INDEX(`belegfID`);
Then to fix item 2, I had to change the first column of table tblBelege
from this:
`belegid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
to this:
`belegid` INT NOT NULL,
...so that they matched the same type as the belegfID column as defined in the other tables. After those two changes, I was able to successfully run your CREATE TABLE `tblBelege` statement.
So, to recap:
Run your create table statements for tblbelegpositionen and
tbleckassenschnittpositionen.
Then run the 2 ALTER statements shown above for item 1.
Modify the first column of table tblBelege to match the column types as defined to belegfID in the other tables for item 2.
Then run your modified CREATE TABLE statement (with the item 2 change applied) to create the tblBelege table.
I'm a little confused about the same FOREIGN KEY referencing 2 different tables, but if it works for you, then ok. (not saying that it cannot be done, I've never used a foreign key that way) Perhaps you meant the opposite, to have a foreign key in the other 2 tables (1 in each table) that refer to tblBelege instead? If so, then you could add unsigned to the type definition for belegfID and it would work, and would not need the change that I mentioned with item 2.
Oh, and after you run the ALTER statements, you can view the table structure by running:
SHOW CREATE TABLE `tblbelegpositionen`;
SHOW CREATE TABLE `tbleckassenschnittpositionen`;
...to get the KEY definition that was added to include with your CREATE TABLE statements. Since they both create the same key, you really only need to run one of those statements and then add the KEY definition to both table statements.
I will apologise now coming from a c++ opp design pattern
For the example three table database (non-Key fields omitted) is it possible sane to cross-reference another table in the REFERENCES section?
CREATE TABLE `Attempt` (
`attempt_ID` INTEGER PRIMARY KEY AUTOINCREMENT,
`username` TEXT NOT NULL,
`assessment_ID` INTEGER NOT NULL,
`timestamp` INTEGER NOT NULL,
FOREIGN KEY(`username`) REFERENCES Students ( username ),
FOREIGN KEY(`assessment_ID`) REFERENCES Assessments ( assessment_ID )
);
CREATE TABLE `Questions` (
`assessment_ID` INTEGER NOT NULL,
`question_number` INTEGER NOT NULL,
PRIMARY KEY(assessment_ID,question_number)
);
CREATE TABLE "Responses" (
`attempt_ID` INTEGER,
`question_number` INTEGER,
PRIMARY KEY(attempt_ID,question_number),
FOREIGN KEY(`attempt_ID`) REFERENCES Attempt ( attempt_ID )
);
The obvious solution is to include the assessment_ID in every question.row
but if we already know the attempt_ID then we can lookup the assessment ID ?
CREATE TABLE "Responses" (
`attempt_ID` INTEGER,
`assessment_ID` INTEGER,
`question_number` INTEGER,
PRIMARY KEY(attempt_ID,question_number),
FOREIGN KEY(`attempt_ID`) REFERENCES Attempt ( attempt_ID )
FOREIGN KEY(`assessment_ID`,`question_number`) REFERENCES `Questions` ( `assessment_ID`,`question_number` )
);
Sub_Reference maybe ?
What you seem to want to do isn't "sane".
This is what you seem to want to do in the Response table.
FOREIGN KEY(question_number)
REFERENCES Questions(assessment_ID, question_number)
That's not "sane". (That's not SQL.) One column (question_number) cannot reference two columns (assessment_ID, question_number).
It's hard to say what you need to do to fix this. The difference between an attempt and a response isn't clear. If a response is a response to a question, then your Responses table should probably look something like this.
CREATE TABLE responses (
attempt_ID integer not null,
assessment_ID integer not null,
question_number integer not null,
primary key (attempt_ID, assessment_ID, question_number),
foreign key (attempt_ID) references Attempt (attempt_ID ),
foreign key (assessment_ID, question_number)
references questions (assessment_ID, question_number)
);
The obvious solution is to include the assessment_ID in every
question.row but if we already know the attempt_ID then we can lookup
the assessment ID?
Whether you can find some way to navigate to the information you want isn't really relevant. In fact, this kind of thing is a problem that the relational model of data was developed to solve. Nowadays, we design databases based on functional dependencies and normalization.
Also, if you're designing a new database in SQLite, you probably shouldn't write the code using MySQL syntax.
I already checked out this question, and thought I had the answer - but then it didn't look right to me.
I have the following pared down example:
CREATE TABLE pipelines (
name VARCHAR NOT NULL,
owner VARCHAR NOT NULL,
description VARCHAR,
PRIMARY KEY (name, owner),
FOREIGN KEY(owner) REFERENCES user (id)
);
CREATE TABLE tasks (
id INTEGER NOT NULL,
title VARCHAR,
pipeline VARCHAR,
owner VARCHAR,
PRIMARY KEY (id),
FOREIGN KEY(pipeline) REFERENCES pipelines (name),
FOREIGN KEY(owner) REFERENCES pipelines (owner)
);
CREATE TABLE user (
id VARCHAR NOT NULL,
name VARCHAR,
password VARCHAR,
PRIMARY KEY (id)
);
pragma foreign_keys=on;
insert into user values ('wayne', '', '');
insert into pipelines values ('pipey', 'wayne', '');
insert into tasks values (1, 'hello', 'pipey', 'wayne');
When executing this code, it bails out:
$ sqlite3 foo.sq3 '.read mismatch.sql'
Error: near line 27: foreign key mismatch
Through the list in the question I cited:
the parent table (user) exists.
the parent columns (name, owner) exist
the parent columns are, in fact, the primary key (I thought that may have been it originally)
the child table references all of the primary key columns in the parent table
So what in the world could be causing this error?
The documentation says:
Usually, the parent key of a foreign key constraint is the primary key of the parent table. If they are not the primary key, then the parent key columns must be collectively subject to a UNIQUE constraint or have a UNIQUE index.
In the pipelines table, neither the name nor the owner columns are, by themselves, unique.
I guess you actually want to have a two-column foreign key in the tasks table:
FOREIGN KEY(pipeline, owner) REFERENCES pipelines(name, owner)
I am trying to run simple query for sqlite which is update a record if not exists.
I could have used Insert or replace but article_tags table doesn't have any primary key as it is a relational table.
How can I write this query for sqlite as if not exists is not supported.
And I don't have idea how to use CASE for this ?
Table Structure:
articles(id, content)
article_tags(article_id, tag_id)
tag(id, name)
SQLITE Incorrect Syntax Tried
insert into article_tags (article_id, tag_id ) values ( 2,7)
if not exists (select 1 from article_tags where article_id =2 AND tag_id=7)
I think the correct way to do this would be to add a primary key to the article_tags table, a composite one crossing both columns. That's the normal way to do many-to-many relationship tables.
In other words (pseudo-DDL):
create table article_tags (
article_id int references articles(id),
tag_id int references tag(id),
primary key (article_id, tag_id)
);
That way, insertion of a duplicate pair would fail rather than having to resort to if not exists.
This seems to work, although I will, as others have also said, suggest that you add some constraints on the table and then simply "try" to insert.
insert into article_tags
select 2, 7
where not exists ( select *
from article_tags
where article_id = 2
and tag_id = 7 )