2014-10-05 66 views
1

我有三个rails对象:UserDemoUserStatsUserDemoUser都有许多与它们相关的统计数据。 UserStats表存储在Postgresql上(使用ActiveRecord)。 DemoUser存储在redis中。 DemoUser的id是一个(随机)字符串。 User的id是一个(标准轨道)递增整数。Rails has_many带整数主键和字符串外键

stats表具有user_id列,该列可以包含User id或DemoUser id。出于这个原因,user_id列是一个字符串,而不是一个整数。

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

我有一些代码来管理这些统计信息。我希望能够通过一个some_user实例(可以是DemoUserUser),然后能够使用id来获取Stats,更新它们等。同样会很高兴能够定义一个has_manyUser模型,所以我可以做的事情一样user.stats

然而,像user.stats行动将创建一个像

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

然后与PG::UndefinedFunction: ERROR: operator does not exist: character varying = integer

012打破了查询

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

编辑:更新的问题,试图让事情尽可能明确。乐于接受编辑或回答问题以澄清任何事情。

+1

'stats.user_id'也不是一个字符串吗?那么你的数据库就会有意义,你的问题就会消失。 – 2014-10-05 20:22:29

+0

'stats.user_id'是一个字符串...我不想将'user.id'更改为一个字符串,或者它可能会创建一个无法预料的问题... – gingerlime 2014-10-05 20:38:52

+0

因此'stats.user_id'是一个字符串,它与来自其他数据库的字符串“id”匹配。但是你不想把'users.id'改成一个字符串,即使它真的是一个来自你的其他数据库的字符串?改变它以符合现实并处理副作用。如果外部'id'结束为'4ed6aa9f30f1b90927000001''或其他非数字字符串?您的模式**必须**符合您的数据,否则您只会有一大堆混乱。要么你在任何地方都使用外部'id's(使用适当的类型),要么你需要一张表来映射它们。 – 2014-10-05 20:47:49

回答

1

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

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


在你的情况,你可以创建用户定义=运营商varchar = string,但会有凌乱的副作用在数据库中的其他地方;例如,它会允许伪造代码,如:

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

运行时没有错误。所以我不会给你这样的代码。如果您真的觉得这是唯一的解决方案(我认为这可能不是正确的),那么请参阅CREATE OPERATORCREATE FUNCTION

+0

我不想在postgres中重载整个'='运算符。这是肯定的。我想知道是否有一种方法可以将整数转换为只在这个表上的字符串(因为翻译非常简单,'42 - >“42”')。我可以在Rails中做这个翻译,但是我不喜欢Rails为处理关系提供的一些糖。 – gingerlime 2014-10-06 08:38:04

+0

@gingerlime不,你不能那样做每个表。只需修复列定义。如果你*真的*无法修复应用程序和列类型,也许你需要一个可更新的视图来将int col转换为字符以获得bug应用程序?最糟糕的情况是保留文本列并使用'BEFORE'触发器将其转换为整数,然后将该值复制到integer类型的第二列,使其成为外键。但我真的无法想象这样做的理由。 – 2014-10-06 08:43:57

+0

谢谢克雷格。 upvoted你的答案。看起来我最终在应用层上解决了这个问题(请参阅我的答案)。该解决方案不是那么复杂,它只是不适合使用rails中的内置方法自动定义外键关系。但我可以很容易地模仿大部分功能。我只是好奇,是否有机会在DB层上做到这一点,但看起来不值得冒这个麻烦。 – gingerlime 2014-10-06 08:49:06

0

目前,我的'解决方案'不是在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)

0

电话,我想我误会了详细以前的问题,因为它被埋在评论中。

(我强烈建议编辑您的问题澄清点,当评论显示有问题时有混淆/不完整)。

你似乎希望从一个整数列到一个字符串列的外键,因为字符串列可能是一个整数,或者可能是一些不相关的字符串。 这就是为什么你不能使它成为一个整数列 - 它不一定是一个有效的数字值,它可能是来自不同系统的文本密钥。

在这种情况下,典型的解决方案应该是有一个合成主键和两个约束,而不是每个系统的密钥,另外还有一个禁止设置的约束。例如。

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(); 

该副本,如果它是一个整数,所以你可以参考它的整数值。

总而言之,虽然我认为整个想法有点不切实际。

+0

感谢您花时间把Craig放在一起。如果我的问题不清楚,我很抱歉。尽管如此,我并不完全确定要改变什么。至于这个答案 - 我不确定我是否完全理解它是诚实的 - 应用程序是否能够引用相同的列/键?否则 - 如果应用程序需要知道两个不同的键,那么它不能实现我不幸的。但也许这只是我的不理解。 – gingerlime 2014-10-06 15:06:48

+0

@gingerlime那么,在第二个例子中,如果文本键的值是整数,则只需将文本键的值复制到第二列中,以便可以为其创建FK引用。 – 2014-10-06 15:12:28

+0

但是,然后应用程序仍然需要引用两个不同的外键,这是我想要避免的。对不起,如果我无法解释清楚。我会尝试更新这个问题 - 即使有点难以解释而没有深入细节(或者我只是不太擅长解释这一点 - 这更可能) – gingerlime 2014-10-06 16:07:56

1

一个选项是在您的stats表中分别有user_iddemo_user_id列。 user_id将是一个整数,您可以将它用作PostgreSQL中的users表的外键,demo_user_id将是一个将链接到您的Redis数据库的字符串。如果你想正确对待数据库中,你会使用一个真正的FK链接stats.user_idusers.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以进行某种定期扫描,以确保它们与Redis中的值相关联,这将是一个不错的主意。

现在您的User可以使用stats.user_id列的标准关联查找统计数据,并且您的DemoUser可以使用stats.demo_user_id

+0

我们通常使用PG中实施的外键约束(使用外国人的宝石)。在这种情况下,这是一个例外 - 因为我们将这与Redis相同 - 我们不能很容易地在PG之外维护引用完整性。实际上,如果它不适用于内存限制 - 我们可能会将这些统计信息保留在redis中。使用两个单独的列是绝对有可能的,但是使代码中的事情变得非常混乱 - 即总是检查它是否是'User'或'DemoUser',然后使用适当的键或具有返回键类型的某个函数。这正是我想要避免的。 – gingerlime 2014-10-07 07:34:58

+0

但是,如果你说'x.stats'之类的东西,那么'x'会照顾到这一点。 – 2014-10-07 17:37:07

+0

是的,这是可能的。当前的代码库并不完全按照这种方式工作。管理统计数据的各种对象传递用户标识以引用不同的数据对象(统计数据仅仅是几个相似对象的简化)。另外,我自己的[答](http://stackoverflow.com/a/26212710/305019)已经做到了这一点,而不添加第二栏,我知道你对参照完整性问题和未来问题非常坚定,但是我看不到我的解决方案是多么糟糕...... – gingerlime 2014-10-07 18:20:19

0

我想我可以为你的问题的解决方案,但也许不是一个大规模更好的:

class User < ActiveRecord::Base 

    has_many :stats, primary_key: "id_s" 

    def id_s 
    read_attribute(:id).to_s 
    end 

end 

依然采用的是第二虚拟列,但也许更方便使用Rails协会使用,是数据库无关。