'How to check if two columns references the same record?

I have 3 tables. User, Task and Tags.

  • A user have many tasks.
  • A user have many tags.
  • A task have many tags.

So for I get this columns:

  • User: id; email
  • Tag: id; user_id; name
  • Task: id; user_id; description

To manage the fact that a task can have many tags, I added a 4th table, taged_tasks, that is a junction table between Tags and Tasks.

  • This table have 3 columns : id, task_id, tag_id.

I want to add a constraint into my migrations, that checks that task_id and tag_id belongs to the same user.

But I don't find how to do that, is it even possible to implement that constraint in DB? Or is rails hook (like before_create) the only way to do this check? Because I don't want to have 'invalid' record into this taged_tasks table.



Solution 1:[1]

It's possible to validate this either at the database level or at the app level. There are multiple ways to do so: in the database, you could add a trigger; in the app, you could use a Rails validation or scope the relation only to tasks/tags of the same user.

With the Rails method, you could do it by defining an actual HABTM relation and scoping it:

class Tag < ApplicationRecord
  belongs_to :user
  has_and_belongs_to_many :tasks, ->(tag) { where(user_id: tag.user_id) }
end

class Task < ApplicationRecord
  belongs_to :user
  has_and_belongs_to_many :tags, ->(task) { where(user_id: task.user_id) }
end

This should prevent invalid relations from being created, but I'm not positive on how strong a guarantee it offers. A validation on the join table would be a little simpler:

class TagTask < ApplicationRecord
  belongs_to :task
  belongs_to :tag

  validate do
    tag.user == task.user
  end
end

The trigger approach is less Rails-y but offers a stronger guarantee that you won't end up with invalid data. This likely has some errors, but something like:

CREATE OR REPLACE FUNCTION check_same_user()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $function$
BEGIN

    if (NOT EXISTS (SELECT 1 FROM tags JOIN tasks USING(user_id) WHERE tag_id = new.tag_id AND task_id = new.task_id)) then
        raise exception 'task and tag belong to different users!';
    end if;
    
    return new;
    
END;
$function$
;

-- run the above trigger before every insert/update
CREATE TRIGGER ensure_same_user
  BEFORE INSERT OR UPDATE
  ON tagged_tasks
  EXECUTE PROCEDURE check_same_user();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Robert Nubel