![](/img/trans.png)
[英]Rails has_many belongs_to foreign_key primary_key confusion
[英]Rails has_many with an integer primary key and a string foreign key
我有三个Rails对象: User
, DemoUser
和Stats
。 User
和DemoUser
都有许多与之关联的统计信息。 User
和Stats
表存储在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
实例(可以是DemoUser
或User
),然后能够使用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 OPERATOR
和CREATE FUNCTION
。
一种选择是在stats
表中具有单独的user_id
和demo_user_id
列。 user_id
将是一个整数,您可以将其用作PostgreSQL中的users
表的外键,而demo_user_id
将是一个链接到Redis数据库的字符串。 如果要正确对待数据库,则可以使用真实的FK将stats.user_id
链接到users.id
以确保引用完整性,并包括CHECK约束以确保stats.user_id
和stats.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.stats
和Stats.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.