Rails 表中没有 Primary Key 怎么办?( 暂时解决 )

blacktulip · June 23, 2015 · Last by blacktulip replied at June 26, 2015 · 3896 hits

暂时解决:

  1. 添加一个 real_id 字段,指定为 primary key
  2. 把原 id 字段更名成 rev_id
  3. 把 real_id 更名为 id
  4. 把 所有涉及用 id 的地方都改成 rev_id ( 血泪 )

似乎 2, 3, 4 是多余的,但是 Rails 的 convention 实在太强,如果主键不叫 id,会造成极大的不便,还是一次改对比较好。

现在还有一个问题就是 primary key 不会 auto increment . 完全不知道原因,也许是先指定后更名造成的后果,当前在 controller 中手动指定 id 加一,暂时先用着。


好吧... 接手了一个老 Rails 项目,原来的作者是个开发 IDE 的牛人,不过他从没搞过 Rails,当初也是现学现写的,一个测试都没有我就先不说了,现在碰上一个坑是这样的:

有一个 model 叫 Resource ( 嗯,你猜对了,routes.rb 里面有一行 resources :resources,不过这不是重点 ) 另有一个 model 叫 ResourceRevision 是用来给 Resource 做版本控制的,就是说 Resource#show 实际上显示的是某一个 ResourceRevision 的内容,每修改一次,会存成一个新的 ResourceRevision

然而, ResourceRevision 对应的数据表 resource_revisions 里面没有 primary key ......

resource_revisions 这个表里面有一个 id 字段和一个 resource_id 字段,resource_id 对应的是这个 ResourceRevision 从属的 Resourceid ,这个正常;但是 id 字段仅在一个 Resource 内部递增,下一个 Resource 又从 1 开始。不知道我说清了没有,举例子:

@revision_1 = ResourceRevision.where(resource_id: 1, id: 1) # 第 1 个 Resource 的第 1 个 ResourceRevision
@revision_2 = ResourceRevision.where(resource_id: 1, id: 2) # 第 1 个 Resource 的第 2 个 ResourceRevision
@revision_3 = ResourceRevision.where(resource_id: 2, id: 1) # 第 2 个 Resource 的第 1 个 ResourceRevision

这三个都是可以的,也就是没有一个 column 是 unique 的。

虽然已经把这两个 key 合做了 index,但是还是有些麻烦,比方说我经常会得到这种错误:ActiveRecord::UnknownPrimaryKey in ResourceRevisionsController#some_method

我也试过 composite_primary_keys 这个 gem,但是用了之后原来所有的取值都无效了,Rails 会把两个 id 合成一个数组当 id 去数据库里面取值,显然取不到,这个 gem 应该只适合特别设计的表结构。

那么现在我该怎么办呢?我在想能不能就另加一个 column,起名 unique_id 之类的,设成 primary key?如果我这样做了会有什么副作用么?有没有更好的解决方案呢?

求帮助,谢谢。

联合主键在数据库设计角度看挺正常的。报错就处理错误。

试一下 self.primary_key = nil

#2 楼 @lolychee 这个试了不行,还是报 ActiveRecord::UnknownPrimaryKey

  1. 增加一个 column real_id,类型为 auto increment
  2. 把 real_id 为空的填充数据
  3. self.primary_key = :real_id

比方说我经常会得到这种错误:ActiveRecord::UnknownPrimaryKey in ResourceRevisionsController#some_method

原因大概是你去 update 了那条没 primary key 的记录,修改代码,改成AR.connection.execute("update xxx set xxx = xx where xx = xx and xxx = xxx;")

如果我这样做了会有什么副作用么?

没副作用,多了一个 int column 而已。其实一般中间表,我也喜欢加一个主键上去,大部分时候中间表还是需要手工维护的,从没有再到有的过程需求迁移一次,好麻烦,直接加上也不占多少空间...而且像 has many through 这种结构还可以改变查询方式。不加只能一个 join 查询,加了之后可以 has many + in 查询,分页和 top N 情况好很多。

#4 楼 @hooopo 其实我刚才试过了,有一些进展

首先增加一个 column,然后类型设成 primary_key,rails 会很牛逼的自动把已有的记录都填好空

现在就有这么个问题...hmm... 这个表里有个叫 id 的 column,但是并不 unique,这个你知道了。然后我加个 real_id 当主键,加上以后有这么个问题:@revision.id 取到的是 real_id 的值,这个 #id 方法是直接对应 primary key 而不是对应一个叫 id 的 column 的。

但是 @revision.id @revision.id= 这样的东西在项目里已经到处都是了,这一下子就全乱套了。

我于是尝试 monkey 之

def id
  self.attrbutes[:id]
end

这个还能 work

def id=(a)
  # 写啥呢
  self.update_attributes{id: a} # 这个不 work ,还是会去更新 real_id lol
end

而且这两个 patch 其实也不能 cover 所有的 case,还有一些散布各处的不知道什么时候会跳出来咬一口,Oh,我说过一个测试都没有么......

直接用 raw sql 也许可以... 我还没有试。我对此并不反感,但是实在是要改的地方太多了,在下定决心之前,还是想找找有没有偷懒的方法 😄

其实仔细想一下,这个要求其实就是这样:

  1. 加一个 primary key column,好让这个记录能 update
  2. @revision.id 还是指向原来的 id column ...

rename id => fake_id 然后替换原有的 id 调用为 fake_id. monkey patch 是在继续挖坑。

#7 楼 @hooopo 嗯,这个我也试了其实,搞来搞去搞不通。其实这个应该是最佳方案了,搞不通也许是因为我现在比较头晕脑胀。等我清醒的时候再试一次,看看能不能搞出来。谢谢

#7 楼 @hooopo 请问 monkey patch 为什么是在继续挖坑呢?

#7 楼 @hooopo update 一下,暂时解决了,谢谢。还有一点小问题不过能 work around

#9 楼 @pathbox 这种情况最直接的解决办法是替换掉 id 的调用。monkey patch 之后改变了原来接口的行为,后来维护楼主项目的人估计又得开帖骂楼主了...

再次强调一下:

monkey patch 的应用场景主要是在外部 plugin 不提供相应API接口的情况下去修复 plugin 的行为或 bug。

楼主的这种 monkey patch 的使用方式上这样的:我自己的代码有问题,不想去修改,反而去改变 AR 的接口行为。这里其实就产生了 Tech Debt,这个债要谁来还呢,应该是下一个接手项目的人...

楼主说还存在一些小问题,其实就是一个小炸弹,不修改 revision 相关的代码还好...

另,楼主一直强调没有测试,其实我觉得不写测试和没有测试就不会改代码都是病。

同意 #11 楼 ,而且当场补上测试再重构很难么……

#11 楼 @hooopo 学习了。测试确实体现其重要性啊

@hooopo 会改啊,就是改起来底气稍显不够 @msg7086 不难,但此表牵连甚广,要写的测试不是一两天的事,所以为了交差,我还是可耻地后补测试了

You need to Sign in before reply, if you don't have an account, please Sign up first.