繁体   English   中英

Rails has_many具有整数主键和字符串外键

[英]Rails has_many with an integer primary key and a string foreign key

我有三个Rails对象: UserDemoUserStats UserDemoUser都有许多与之关联的统计信息。 UserStats表存储在Postgresql中(使用ActiveRecord)。 DemoUser存储在DemoUser中。 DemoUser的ID是一个(随机)字符串。 User的ID是一个(标准轨道)递增整数。

stats表具有一个user_id列,其中可以包含User ID或DemoUser ID。 因此, user_id列是字符串,而不是整数。

没有一种简单的方法可以将随机字符串转换为整数,但是有一种非常简单的方法可以将整数id转换为字符串( 42 -> "42" )。 这些ID 保证不会重叠(永远不会有与DemoUser具有相同ID的User实例)。

我有一些代码来管理这些统计信息。 我希望能够传递some_user实例(可以是DemoUserUser ),然后能够使用id来获取Stats ,更新它们等。此外,能够定义一个has_many用于User模型,所以我可以做类似user.stats事情

但是,类似user.stats操作会创建类似

SELECT "stats".* FROM "stats" WHERE "stats"."user_id" = 42

然后用PG::UndefinedFunction: ERROR: operator does not exist: character varying = integer中断PG::UndefinedFunction: ERROR: operator does not exist: character varying = integer

有没有办法让数据库(Postgresql)或Rails对JOIN上的ID进行自动翻译? (从整数到字符串的转换应该很简单,例如42 -> "42"

编辑 :更新了问题,试图使事情尽可能清楚。 很高兴接受编辑或回答问题以澄清任何内容。

您不能在没有内置相等运算符的两种类型之间定义外键。

正确的解决方案是将字符串列更改为整数。


在您的情况下,您可以varchar = string创建一个用户定义的=运算符,但这会在数据库的其他地方产生混乱的副作用; 例如,它将允许伪造代码,例如:

SELECT 2014-01-02 = '2014-01-02'

运行没有错误。 因此,我不会为您提供执行此操作的代码。 如果您确实认为这是唯一的解决方案(我认为这可能是不正确的),请参阅CREATE OPERATORCREATE FUNCTION

一种选择是在stats表中具有单独的user_iddemo_user_id列。 user_id将是一个整数,您可以将其用作PostgreSQL中的users表的外键,而demo_user_id将是一个链接到Redis数据库的字符串。 如果要正确对待数据库,则可以使用真实的FK将stats.user_id链接到users.id以确保引用完整性,并包括CHECK约束以确保stats.user_idstats.demo_user_id恰好是其中之一。为NULL:

check (user_id is null <> demo_user_id is null)

当然,您必须与ActiveRecord进行斗争才能适当地限制数据库,AR不相信FK和CHECK之类的奇特事物,即使它们对于数据完整性是必需的。 不过,您必须demo_user_id控制demo_user_id ,以确保它们与Redis中的值链接起来,进行某种定期扫描是个好主意。

现在,您的User可以使用与stats.user_id列的标准关联来查找统计信息,而您的DemoUser可以使用stats.demo_user_id

目前,我的“解决方案”不是在Rails中使用has_many ,而是在必要时可以在模型中定义一些辅助函数。 例如

class User < ActiveRecord::Base
  # ...
  def stats
    Stats.where(user_id: self.id.to_s) 
  end
  # ...
end

另外,我将定义一些帮助程序范围以帮助执行to_s翻译

class Stats < ActiveRecord::Base
  scope :for_user_id, -> (id) { where(user_id: id.to_s) }
  # ...
end

这应该允许像

user.statsStats.for_user_id(user.id)

我想我之前误解了您的问题的详细信息,因为它已被藏在评论中。

(我强烈建议编辑您的问题,以在评论显示问题中存在混淆/不完整之处时澄清要点)。

您似乎想要从整数列到字符串列的外键,因为字符串列可能是整数,或者可能是一些不相关的字符串。 这就是为什么您不能将其设置为整数列-它不一定是有效的数字值,它可能是来自其他系统的文本键。

在这种情况下,典型的解决方案是具有一个合成主键和两个UNIQUE约束,一个约束来自每个系统,再加上一个CHECK约束,可防止同时设置这两个约束。 例如

CREATE TABLE my_referenced_table (
   id serial,
   system1_key integer,
   system2_key varchar,
   CONSTRAINT exactly_one_key_must_be_set 
     CHECK (system1_key IS NULL != system2_key IS NULL),
   UNIQUE(system1_key),
   UNIQUE(system2_key),
   PRIMARY KEY (id),
   ... other values ...
);

然后,您可以从整数键表中引用一个引用system1_key的外键。

这不是完美的,因为它不能防止同一值出现在两个不同的行中,一个用于system1_key ,另一个用于system2_key

因此,替代方法可能是:

CREATE TABLE my_referenced_table (
   the_key varchar primary key,
   the_key_ifinteger integer,
   CONSTRAINT integerkey_must_equal_key_if_set
     CHECK (the_key_ifinteger IS NULL OR (the_key_ifinteger::varchar = the_key)),
   UNIQUE(the_key_ifinteger),
   ... other values ...
);

CREATE OR REPLACE FUNCTION my_referenced_table_copy_int_key()
RETURNS trigger LANGUAGE plpgsql STRICT
AS $$
BEGIN
  IF NEW.the_key ~ '^[\d]+$' THEN
     NEW.the_key_ifinteger := CAST(NEW.the_key AS integer);
  END IF;
  RETURN NEW;
END;
$$;

CREATE TRIGGER copy_int_key
BEFORE INSERT OR UPDATE ON my_referenced_table
FOR EACH ROW EXECUTE PROCEDURE my_referenced_table_copy_int_key();

如果整数是整数,它将复制整数值,因此您可以引用它。

总而言之,尽管我认为整个想法有些跷。

我想我可以为您的问题提供解决方案,但可能不是一个更好的解决方案:

class User < ActiveRecord::Base

  has_many :stats, primary_key: "id_s"

  def id_s
    read_attribute(:id).to_s
  end

end

仍然使用第二个虚拟列,但与Rails关联一起使用可能更方便,并且与数据库无关。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM