我有三个导轨对象:User
,DemoUser
和Stats
。 User
和DemoUser
都有许多与之关联的统计信息。 User
和Stats
表存储在Postgresql上(使用ActiveRecord)。 DemoUser
存储在redis中。 DemoUser
的id是(随机)字符串。 User
的id是(标准轨道)递增整数。
stats
表格的user_id
列可以包含User
ID或DemoUser
ID。因此,user_id
列是一个字符串,而不是整数。
从随机字符串转换为整数并不是一种简单的方法,但是将整数id转换为字符串(42 -> "42"
)是一种非常简单的方法。这些ID 保证不会重叠(不会有User
个实例与DemoUser
具有相同ID。
我有一些管理这些统计数据的代码。我希望能够传递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
有没有办法让数据库(Postgresql)或Rails在JOIN上自动转换id?(从整数到字符串的转换应该很简单,例如{{1 }})
编辑:更新问题,尽量让事情变得清晰。很高兴接受编辑或回答问题以澄清任何事情。
答案 0 :(得分:1)
您无法在没有内置相等运算符的两种类型之间定义外键。
正确的解决方案是将字符串列更改为整数。
在你的情况下,你可以为=
创建一个用户定义的varchar = string
运算符,但这会在数据库的其他地方产生混乱的副作用;例如,它会允许虚假代码,如:
SELECT 2014-01-02 = '2014-01-02'
运行没有错误。所以我不打算给你这么做的代码。如果您真的觉得这是唯一的解决方案(我认为这可能不正确),请参阅CREATE OPERATOR
和CREATE FUNCTION
。
答案 1 :(得分:1)
一种选择是在user_id
表中添加单独的demo_user_id
和stats
列。 user_id
将是一个整数,您可以将其用作PostgreSQL中users
表的外键,demo_user_id
将是一个链接到Redis数据库的字符串。如果您想正确处理数据库,可以使用真正的FK将stats.user_id
链接到users.id
以确保参照完整性,并且包含CHECK约束以确保{{1}中的一个}和stats.user_id
为NULL:
stats.demo_user_id
当然,你必须对ActiveRecord进行一些正确约束你的数据库,AR不相信像FK和CHECK这样的花哨的东西,即使它们是数据完整性所必需的。您必须手动控制check (user_id is null <> demo_user_id is null)
,但是某些定期扫描以确保它们与Redis中的值相关联将是一个好主意。
现在,您的demo_user_id
可以使用与User
列的标准关联来查找统计信息,而stats.user_id
可以使用DemoUser
。
答案 2 :(得分:0)
暂时,我的解决方案&#39;不要在Rails中使用has_many
,但如果需要,我可以在模型中定义一些辅助函数。 e.g。
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)
答案 3 :(得分:0)
我想我之前误解了你的问题的一个细节,因为它被埋没在评论中。
(我强烈建议编辑您的问题,以便在评论显示问题中存在令人困惑/不完整的内容时澄清要点)。
您似乎需要从整数列到字符串列的外键,因为字符串列可能是整数,或者可能是某些不相关的字符串。 那是为什么你不能把它变成一个整数列 - 它不一定是有效的数字值,它可能是来自不同系统的文本密钥。
在这种情况下,典型的解决方案是使用合成主键和两个UNIQUE
约束,一个用于来自每个系统的键,另外一个CHECK
约束阻止两者设置。 E.g。
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();
如果它是一个整数,则复制整数值,因此你可以引用它。
总而言之,我认为整个想法有点不确定。
答案 4 :(得分:0)
我想我可能有一个解决你的问题的方法,但也许不是一个更好的问题:
class User < ActiveRecord::Base
has_many :stats, primary_key: "id_s"
def id_s
read_attribute(:id).to_s
end
end
仍然使用第二个虚拟列,但使用Rails关联可能更方便,并且与数据库无关。