mathr / blog / #

Ruby On Rails has_and_belongs_to_many id gotcha

Remember when they said that "every table in your database should have an 'id' field as primary key"? Well they lied, because in the case where you have a has_and_belongs_to_many relation, having a field called 'id' in the association table breaks code horribly. Somehow if you have an 'id' field in the groups_users table, doing

group.users.each{|user| user.id}

will not give you the id of the user, but some other value (probably the id of the association entry between the group and the user, but I haven't verified this). Strangely,

group.users.each{|user| user.user_id}

has the correct id instead, even though User should have no user_id field. Naturally, this causes no end of trouble!

It turns out it is safe to remove the 'id' field from the groups_users table entirely, leaving it without a primary key. And magically my code works as expected now, with user.id really being user.id everywhere.

Here is the SQLite3 schema for my project so far

BEGIN TRANSACTION;

CREATE TABLE users (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ,
    login VARCHAR(255) NOT NULL ,
    password VARCHAR(255) NOT NULL
);

CREATE TABLE groups (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT ,
    name VARCHAR(255) NOT NULL
);

CREATE TABLE groups_users (
    user_id  INTEGER NOT NULL CONSTRAINT fk_user_id  REFERENCES users(id)  ON DELETE CASCADE ,
    group_id INTEGER NOT NULL CONSTRAINT fk_group_id REFERENCES groups(id) ON DELETE CASCADE
);


CREATE TRIGGER fki_users_uniqueness
BEFORE INSERT ON users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM users WHERE login = NEW.login) IS NOT NULL)
        THEN RAISE(ABORT,
            'insert on table "users" violates uniqueness constraint on "login"')
    END;
END;

CREATE TRIGGER fku_users_uniqueness
BEFORE UPDATE ON users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM users WHERE login = NEW.login) IS NOT NULL)
        THEN RAISE(ABORT,
            'update on table "users" violates uniqueness constraint on "login"')
    END;
END;

CREATE TRIGGER fki_groups_uniqueness
BEFORE INSERT ON groups
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM groups WHERE name = NEW.name) IS NOT NULL)
        THEN RAISE(ABORT,
            'insert on table "groups" violates uniqueness constraint on "name"')
    END;
END;

CREATE TRIGGER fku_groups_uniqueness
BEFORE UPDATE ON groups
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM groups WHERE name = NEW.name) IS NOT NULL)
        THEN RAISE(ABORT,
            'update on table "groups" violates uniqueness constraint on "name"')
    END;
END;

CREATE TRIGGER fki_groups_users_uniqueness
BEFORE INSERT ON groups_users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT user_id FROM groups_users WHERE user_id = NEW.user_id AND group_id = NEW.group_id) IS NOT NULL)
        THEN RAISE(ABORT,
            'insert on table "groups_users" violates uniqueness constraint on "user_id, group_id"')
    END;
END;

CREATE TRIGGER fki_groups_users_user_id
BEFORE INSERT ON groups_users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM users WHERE id = NEW.user_id) IS NULL)
        THEN RAISE(ABORT,
            'insert on table "groups_users" violates foreign key constraint on "user_id"')
    END;
END;

CREATE TRIGGER fki_groups_users_group_id
BEFORE INSERT ON groups_users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM groups WHERE id = NEW.group_id) IS NULL)
        THEN RAISE(ABORT,
            'insert on table "groups_users" violates foreign key constraint on "group_id"')
    END;
END;

CREATE TRIGGER fku_groups_users_uniqueness
BEFORE UPDATE ON groups_users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT user_id FROM groups_users WHERE user_id = NEW.user_id AND group_id = NEW.group_id) IS NOT NULL)
        THEN RAISE(ABORT,
            'update on table "groups_users" violates uniqueness constraint on "user_id, group_id"')
    END;
END;

CREATE TRIGGER fku_groups_users_user_id
BEFORE UPDATE ON groups_users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM users WHERE id = NEW.user_id) IS NULL)
        THEN RAISE(ABORT,
            'update on table "groups_users" violates foreign key constraint on "user_id"')
    END;
END;

CREATE TRIGGER fku_groups_users_group_id
BEFORE UPDATE ON groups_users
FOR EACH ROW BEGIN
    SELECT CASE
        WHEN ((SELECT id FROM groups WHERE id = NEW.group_id) IS NULL)
        THEN RAISE(ABORT,
            'update on table "groups_users" violates foreign key constraint on "group_id"')
    END;
END;

CREATE TRIGGER fkd_groups_users_user_id
BEFORE DELETE ON users
FOR EACH ROW BEGIN
    DELETE FROM groups_users WHERE user_id = OLD.id;
END;

CREATE TRIGGER fkd_groups_users_group_id
BEFORE DELETE ON groups
FOR EACH ROW BEGIN
    DELETE FROM groups_users WHERE group_id = OLD.id;
END;

INSERT INTO "users" (id, login, password) VALUES (0, 'admin', 'but-we-dont-want-to-give-you-that');
INSERT INTO "groups" (id, name) VALUES (0, 'admin');
INSERT INTO "groups_users" (user_id, group_id) VALUES (0, 0);

COMMIT;

Look at all those lovely triggers! Tasty referential data integrity!

The Sky At Night - John Peel Tribute: Session #1 - 01 - Cathode

confused

motherboard 2 bead