【问题标题】:Ruby Model Relationships - Three way RelationshipRuby 模型关系 - 三向关系
【发布时间】:2018-02-25 18:37:19
【问题描述】:

我有一个包含 3 个特定模型的应用;用户、国家和城市。每个人的理想关系如下:

用户

  • has_many :国家
  • has_many :cities, through: :country

国家

  • has_many :users
  • has_many :城市

城市

  • has_many :users
  • belongs_to :country

这些关系中的大多数都相当直接,但是,用户与城市的关系给我带来了一些问题。我希望能够为用户分配一个城市,只要该用户已分配到与该城市关联的国家。现在我什至无法为用户分配城市,更不用说为限制制定正确的逻辑了。

到目前为止,我能够阅读和搜索的内容导致以下内容。

用户.rb

class User < ApplicationRecord

  before_save { self.email = email.downcase}
  #attr_accessible :username, :email
  validates_confirmation_of :password
  has_secure_password

  validates :username, presence: true, length: { maximum: 25 }, uniqueness: {case_sensitive: false}
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX }
  validates :password, presence: true, confirmation: true, length: { minimum: 6 }
  validates :password_confirmation, presence: true

  has_many :trips
  has_many :countries, through: :trips
  has_many :cities, through: :trips

end

Country.rb

class Country < ApplicationRecord

  has_many :trips
  has_many :cities
  has_many :users, through: :trips

end

City.rb

class City < ApplicationRecord

  has_many :trips
  belongs_to :country

end

Trip.rb

class Trip < ApplicationRecord

  belongs_to :country
  belongs_to :user
  belongs_to :city

end

【问题讨论】:

  • 你能不能只创建两个标准连接表:countries_userscities_users,并在后者上写一个模型验证来断言该城市已被分配到用户?
  • 旁注:您可能还需要某种删除前或删除后的逻辑,以防止用户被未分配来自某个国家/地区,但仍被分配到城市。 (即,要么禁止此类删除,要么删除其他关联作为事务的一部分。)
  • 您不能为用户分配一个城市,因为用户有_many 个城市?用户需要属于某个城市才能将城市分配给用户!您真正想要的是将访问分配给用户并将城市分配给访问。将您的问题更新为有意义的问题,也许我可以提供进一步的帮助。放弃它给你带来问题的简化,让你认为你应该能够做一些违反你要求的事情

标签: ruby-on-rails ruby ruby-on-rails-3 activerecord


【解决方案1】:

希望能够为用户分配城市,只要用户有 被分配到与城市相关的国家

你不能也不应该想要。

创建一个国家。创建一个城市并将其分配给一个国家。创建旅行或访问,将访问或旅行分配到城市,然后将用户添加到该旅行或访问。瞧!你有你需要的关系!我想。

一个城市属于一个国家 一次旅行属于一个城市 一次访问属于一个城市 一个用户有_many访问 用户 has_many trips 因为 trips 和 visits 都属于一个城市,所以你可以通过连接两边的关系拥有 has_many,因此 visits 和 trips 表需要城市和用户 ID 而且你有你需要的关系和你需要的逻辑。

不要尝试做违反您要求的事情,例如直接将城市加入用户,因为无法将城市分配给用户。事实上,只是为了让您更加头疼,您甚至可能想要多次访问许多国家的许多城市。在这种情况下,您希望将访问分配给旅行而不是城市。

这是逻辑问题,不是编码问题。

更新响应cmets

class User < ApplicationRecord
  has_many :trips
  has_many :cities, through: :trips
end

class Trip < ApplicationRecord
  belongs_to :user
  belongs_to :city
end

class City < ApplicationRecord
  has_many :trips
  has_many :users, through: :trips
end

然后在控制台中

Loading development environment (Rails 5.1.4)
2.4.0 :001 > u = User.create(name: "Fred")
   (0.0ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "Fred"], ["created_at", "2017-09-17 14:24:30.720005"], ["updated_at", "2017-09-17 14:24:30.720005"]]
   (16.3ms)  commit transaction
 => #<User id: 1, name: "Fred", created_at: "2017-09-17 14:24:30", updated_at: "2017-09-17 14:24:30"> 
2.4.0 :004 > c = City.create(name: "Leeds")
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "cities" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "Leeds"], ["created_at", "2017-09-17 14:26:19.293166"], ["updated_at", "2017-09-17 14:26:19.293166"]]
   (33.9ms)  commit transaction
 => #<City id: 1, name: "Leeds", created_at: "2017-09-17 14:26:19", updated_at: "2017-09-17 14:26:19"> 
2.4.0 :005 > t = Trip.create(name: "T1", user: u, city: c) 
   (0.2ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "trips" ("user_id", "city_id", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["user_id", 1], ["city_id", 1], ["name", "T1"], ["created_at", "2017-09-17 14:28:44.656390"], ["updated_at", "2017-09-17 14:28:44.656390"]]
   (26.0ms)  commit transaction
 => #<Trip id: 1, user_id: 1, city_id: 1, name: "T1", created_at: "2017-09-17 14:28:44", updated_at: "2017-09-17 14:28:44"> 
2.4.0 :006 > t.user.name#
 => "Fred" 
2.4.0 :007 > t.city.name
 => "Leeds" 
2.4.0 :008 > t.name
 => "T1" 

这就是正常的 has_many through 的工作方式,但您希望在 trips 模型中使用 has_many 城市,所以我会尽快更新它的工作方式

好的,要实现下一个阶段,其中一个旅行有许多城市,一个城市 has_many trips 请注意关系两侧的 has_many,您将需要一个新的交叉引用表,可能称为 trip_city_xref,其复合主键为city_id 和 trip_id

这在 Rails 中是一种我讨厌的 has_and_belongs_to_many 关系。我只认为 has_and_belongs_to_many 是 Rails 唯一出错的地方。这是一个捷径。其他人会不同意,但这就是我用一种没有那种捷径的语言来设置它的方式。我认为它更清晰,更不模棱两可。所以我会设置这样的东西

class Trip < ApplicationRecord
  belongs_to :user
  belongs_to: :city_trip_xref and  
  has_many :cities, through: :city_trip_xref
end

class City < ApplicationRecord
  has_many :trips, through: :city_trip_xref
  has_many :users, through: :trips
end

class CityTripXref < ApplicationRecord
  belongs_to :trip
  belongs_to :city
end

这只是我的意见,所以请随时阅读 has_and_belongs_to_many here 那么你现在如何处理你的用户呢?再次,我会尽快更新

更新 User.cities 所以你的用户模型可能看起来像这样

class User < ApplicationRecord
  has_many :trips
  has_many :city_trip_xrefs, through: :trips
  def cities
    trips.map(&:city_trip_xrefs).flatten.map(&:city)
  end
end

要获取用户在旅途中访问的所有城市,您可以像这样展平和映射关系 u = 用户优先 u.trips.map(&:city_trip_xrefs).flatten.map(&:city)

所以你可以在你的用户类上创建一个方法来使用上面的用户模型来获取城市。至于将城市分配给用户,无论如何您都不会真的想直接这样做。您将设置一次旅行,将用户和城市添加到该旅行中,然后您就拥有了现实世界示例中可能需要的所有功能,除非您的要求有所不同

使用上面在控制台中创建的记录加上 city_trip_xref 表中的记录将所有内容放在一起,以加入一个现有城市的多对多城市旅行和您现在可以进行的旅行

首先在控制台中创建外部参照

 ctx = CityTripXref.new
 => #<CityTripXref id: nil, city_id: nil, trip_id: nil, created_at: nil, updated_at: nil> 
2.4.0 :008 > ctx.city = City.first
 => #<City id: 1, name: "Leeds", created_at: "2017-09-17 14:26:19", updated_at: "2017-09-17 14:26:19"> 
2.4.0 :009 > ctx.trip = Trip.first
 => #<Trip id: 1, user_id: 1, city_id: 1, name: "T1", created_at: "2017-09-17 14:28:44", updated_at: "2017-09-17 14:28:44"> 
2.4.0 :010 > ctx.save

接下来获取第一个用户第一次旅行的第一个城市的名称

2.4.0 :028 > u = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
 => #<User id: 1, name: "Fred", created_at: "2017-09-17 14:24:30", updated_at: "2017-09-17 14:24:30"> 
2.4.0 :029 > u.cities.first.name
  Trip Load (0.1ms)  SELECT "trips".* FROM "trips" WHERE "trips"."user_id" = ?  [["user_id", 1]]
  CityTripXref Load (0.1ms)  SELECT "city_trip_xrefs".* FROM "city_trip_xrefs" WHERE "city_trip_xrefs"."trip_id" = ?  [["trip_id", 1]]
  City Load (0.1ms)  SELECT  "cities".* FROM "cities" WHERE "cities"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 => "Leeds" 
2.4.0 :030 >

希望这是有道理的。

【讨论】:

  • 好的,为了避免增加混乱(主要是我自己),我取消了访问模型。我已将上面的代码编辑为我认为您所建议的内容。它仍然不起作用,我无法分配user1.cities &lt;&lt; [city1],但它看起来很接近。
  • 当然你不能。你需要经历旅行。我不认为你真的明白你想要做什么。我将很快更新我的答案,说明您设置的内容应该如何工作。但我仍然不认为这是你想要的。就我个人而言,在我写了一个 UML 用例或至少写出一个规范之后,我会回到这个话题
猜你喜欢
  • 1970-01-01
  • 2013-03-25
  • 1970-01-01
  • 2011-08-01
  • 1970-01-01
  • 2018-04-05
  • 1970-01-01
  • 1970-01-01
  • 2011-10-05
相关资源
最近更新 更多