【问题标题】:Rails has_many with an integer primary key and a string foreign keyRails has_many 带有一个整数主键和一个字符串外键
【发布时间】:2014-10-05 20:03:18
【问题描述】:

我有三个 rails 对象:UserDemoUserStatsUserDemoUser 都有许多与之相关的统计数据。 UserStats 表存储在 Postgresql 上(使用 ActiveRecord)。 DemoUser 存储在 redis 中。 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,更新它们等。能够为User 模型定义has_many 也很好,所以我可以做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?(从整数到字符串的翻译应该很简单,例如 42 -> "42" )

编辑:更新了问题以尽量使事情清楚。很高兴接受编辑或回答问题以澄清任何事情。

【问题讨论】:

  • stats.user_id 也是一个字符串不是更有意义吗?然后你的数据库就有意义了,你的问题就会消失。
  • stats.user_id 是一个字符串...而且我不想将user.id 更改为一个字符串,否则它可能会产生一大堆无法预料的问题...
  • 所以stats.user_id 是一个字符串,它与来自其他数据库的字符串id 匹配。但是您不想将 users.id 更改为字符串,即使它确实是来自其他数据库的字符串?更改它以匹配现实并处理副作用。如果外部id 最终变成'4ed6aa9f30f1b90927000001' 或其他一些非数字字符串会怎样?您的架构必须与您的数据相匹配,否则您将遇到一大堆混乱。要么在任何地方使用外部ids(使用正确的类型),要么需要一个表格来映射它们。
  • 我认为你误解了一些事情。也许我没有正确解释? stats.user_id 是一个字符串,它可以是"234"(匹配234users.id),也可以是一个外部字符串"abc",它匹配同一代码库中使用的NoSQL 数据库上的ID。
  • 我认为您需要更好地描述您的数据。如果您真的想在一列中存储两种完全不同的东西,那么您手头上就会一团糟。

标签: ruby-on-rails postgresql foreign-keys nosql


【解决方案1】:

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

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


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

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

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

【讨论】:

  • 我不想在 postgres 中重载整个 = 运算符。这是肯定的。我想知道是否有一种方法可以仅在此表上从整数转换为字符串(因为转换非常简单42 -> "42")。我可以在 Rails 中进行这种翻译,但是我不“享受” Rails 为处理关系提供的一些糖分。
  • @gingerlime 不,您不能按表执行此操作。只需修复列定义。如果您真的 无法修复应用程序和列类型,也许您需要一个简单可更新的视图来将有问题的应用程序的 int col 转换为 char?最坏的情况是保留文本列并让BEFORE 触发器将其转换为整数并将值复制到整数类型的第二列,使该列成为外键。但我实在想不出这样做的理由。
  • 谢谢克雷格。赞成你的答案。看起来我最终在应用层解决了这个问题(见我的回答)。解决方案并没有那么复杂,它只是不适合 100% 使用 rails 中的内置方式来自动定义外键关系。但我可以很容易地模仿大部分功能。我只是好奇是否有任何方法可以在 DB 层上执行此操作,但看起来不值得麻烦。
【解决方案2】:

一种选择是在您的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 之一为空:

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

【讨论】:

  • 我们通常使用在 PG 中强制执行的外键约束(使用 foreigner gem)。在这种情况下,这是个例外——因为我们将其视为与 Redis 相同的情况——我们不能很容易地在 PG 之外维护引用完整性。事实上,如果不是因为内存限制——我们可能会将这些统计信息保存在 redis 中。使用两个单独的列绝对是可能的,但会使代码中的事情变得非常混乱 - 即总是检查这是 User 还是 DemoUser 然后使用适当的键,或者具有一些返回键类型的函数。这是我试图避免的。
  • 但是如果你说像 x.stats 这样的话,那么 x 会处理这个问题。
  • 是的,这是可能的。当前的代码库并不完全以这种方式工作。管理 stats 的各种对象传递一个用户 id 来引用不同的数据对象(stats 只是几个类似对象的简化)。另外,我自己的answer 已经在没有添加第二列的情况下做到了这一点,我知道你对参照完整性问题和未来问题非常坚定,但我看不出我的解决方案有多糟糕......跨度>
  • ...顺便说一句,感谢您的耐心等待,我真的在努力理解权衡取舍——不要为了它而固执。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

    我想我之前误解了您问题的一个细节,因为它被埋在了 cmets 中。

    (我强烈建议编辑您的问题,以便在 cmets 显示问题中存在令人困惑/不完整的内容时澄清要点)。

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

    在这种情况下,典型的解决方案是使用合成主键和两个 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();
    

    如果它是一个整数,它会复制整数值,所以你可以引用它。

    总而言之,虽然我认为整个想法有点不确定。

    【讨论】:

    • 感谢您抽出宝贵的时间整理 Craig。如果我的问题不清楚,我很抱歉。我不完全确定要更改什么以使其更清晰。至于这个答案 - 老实说,我不确定我是否完全理解 - 应用程序是否能够引用相同的列/键?否则 - 如果应用程序需要知道两个不同的键,那么不幸的是它无法实现我想要的。但也许这只是我的理解不足。
    • @gingerlime 好吧,在第二个示例中,如果它们是整数,您只是将文本键的值复制到第二列中,因此您可以创建对它的 FK 引用。
    • 但是应用程序仍然需要引用两个不同的外键,这是我试图避免的。抱歉,如果我不能清楚地解释这一点。我会尝试更新这个问题——即使不深入细节有点难以解释(或者我只是不太擅长解释这个问题——这更有可能)
    • 我修改了问题。希望它能消除任何困惑并更好地解释事情(?)
    • 我认为我们可能会在stats 中使用单独的user_id intdemo_user_id text 列。理想情况下,使用“完全是 NULL”约束,但 Rails 意识形态会让你为此而战。
    【解决方案5】:

    我想我可能对您的问题有一个解决方案,但也许不是一个更好的解决方案:

    class User < ActiveRecord::Base
    
      has_many :stats, primary_key: "id_s"
    
      def id_s
        read_attribute(:id).to_s
      end
    
    end
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-06
      • 1970-01-01
      • 2012-12-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多