用于非对等连接的Rails ActiveRecord关联



我们有一个跟踪部门资产注册的Rails应用程序,包括子网和IP地址。部门与子网和ip都有一对多的关联。除了显示某个部门的ip地址(在其他部门的子网内,下面是@dept_ips),我们还需要显示该部门子网内其他部门的ip地址(下面是@others_ips)

部门模型:

class Department < ApplicationRecord

has_many(:subnets, class_name: "Subnet", foreign_key: :department_id)
has_many(:ips, class_name: "Ip", foreign_key: :department_id)
...
end

子网模型有以下方法来获取IP地址:

class Subnet < ApplicationRecord
def ips
Ip.for_subnet(subnet)
end
...
end

在IP模型中引用此方法:

class Ip < ApplicationRecord
def self.for_subnet(subnet)
where("ip << '#{subnet.to_cidr}'")
end
...
end

对于子网和IP地址也有相关的信息:

  • 注册子网到其他表的链接,如描述信息、防火墙信息等
  • 注册ip链接到其他表:主机名,敏感数据级别等

加载Other的IP地址和相关信息的查询速度很慢。使用即时加载有所帮助,但索引页加载速度仍然很慢。@dept_ips加载正常

@others_ips = @department.subnets
.eager_load(ips:[:calc_ip, 
:host, 
{subnet: :fw_subnet}] )
.order('ip.ip')
.map(&:ips)
.flatten
@dept_ips = @department.ips
.eager_load(:calc_ip,
:host, 
{subnet: :fw_subnet})
.order('ip.ip')

由于SQL可以生成所需的信息,我尝试对服务和实体使用原始SQL。这工作,但我不能得到一个系统测试工作,因为实体没有'dom_id'。或者至少我不知道如何为实体创建'dom_id'

我真正想要的是一个使用非equi连接的Rails关联。

可以在Rails中编写自定义连接。例如,

@subnet = Subnet.find_by(subnet: 'cidr')
@subnet_ips = @subnet.joins("inner join ips on ip <<= subnet")
@ip = @subnet_ips.first 

@containing_subnet = @ip.joins("inner join subnets on subnet >>= ip")

…关联总是基于相等:https://guides.rubyonrails.org/association_basics.html

(仅供参考,非平等关系实际上非常有用:https://learnsql.com/blog/sql-non-equi-joins-examples/)

我特别需要一个PostgreSQLinetcidr数据类型之间的非对等连接,特别是' contains by '和' contains '操作符:

SELECT * FROM IPs 
INNER JOIN Subnets
ON IPs.ip << Subnets.subnet;

有关inetcidr数据类型,以及'<<'(contains by)和'>>'(contains)操作符的更多信息,请参阅PostgreSQL文档https://www.postgresql.org/docs/14/functions-net.html。

使用Active Record,当两个模型之间的关系不是基于相等时,不可能创建"有许多/属于"的关联。

当我们需要将IP地址或子网(或两者)关联到其他表时,自定义连接的性能不佳。

解决方案是在IP地址和子网之间建立一个交集表。但是,由于IP地址来来往往,当子网大小改变时(即掩码长度改变),它们所包含的子网也随之改变,因此维护一个实际的交集表是不切实际的。答案吗?数据库视图、只读模型和has_one_throughhas_many_through关联

  1. 使用非equi join定义数据库视图:
CREATE OR REPLACE VIEW ip_subnet_link AS
SELECT i.id as ip_id, s.id AS subnet_id
FROM ip_addresses i
INNER JOIN subnets s ON i.ip << s.subnet;
  1. 并创建一个表示该视图的只读模型:
class IPSubnetLink < ApplicationRecord
self.table_name = "ip_subnet_link"
self.primary_key = "ip_id"
belongs_to(:subnet, class_name: "Subnet", foreign_key: :subnet_id)
belongs_to(:ip, class_name: "Ip", foreign_key: :ip_id)
attribute(:ip_id, :uuid)
attribute(:subnet_id, :uuid)
def readonly?
true
end
end
  1. 最后在子网和IP模型中使用has_one_through,has_many_through关系将IP地址连接到子网:
class Ip < ApplicationRecord
has_one(:subnet_ip, class_name: "IPSubnetLink", foreign_key: :ip_id)
has_one(:subnet, through: :subnet_ip, source: :subnet)
...
end
class Subnet < ApplicationRecord
has_many(:subnet_ips, class_name: "IPSubnetLink", foreign_key: :subnet_id)
has_many(:ips, through: :subnet_ips, class_name: "IP")

...
end

等。瞧!

@others_ips = @department.subnets
.ips
.eager_load(:calc_ip, 
:host, 
{subnet: :fw_subnet})
.order('ip.ip')

解决方案简单易懂,但性能更好。

最新更新