Я разрабатываю плагин для Foreman (v3.5.1) для добавления поддержки записей CNAME в модель сетевого интерфейса (Nic::Base) : ForemanCnames (WIP). Сначала я покажу вам нужные мне ассоциации, где красные стрелки указывают на несуществующую связь:
Хотя я могу получить (известные) CNAME от (известного) сетевого адаптера, я не могу получить сетевой адаптер или домен для данного CNAME.
irb(main):001:0> n = Nic::Base.find(7958)
=> <Nic::Managed id: 7958, name: "satellite-test-01.scc.kit.edu", ...>
irb(main):002:0> a = n.host_aliases.first
=> <ForemanCnames::HostAlias id: 5, name: "newer-alias", nics_id: 7958, domains_id: 1>
irb(main):004:0> a.nic
=> nil
irb(main):006:0> a.domain
=> nil
irb(main):007:0> n.domain
=> <Domain id: 1, name: "scc.kit.edu", ...>
Вот как выглядит HostAlias
module ForemanCnames
class HostAlias < ActiveRecord::Base
belongs_to :domain
belongs_to :nic, :class_name => '::Nic::Base', :inverse_of => :host_aliases
validates :nics_id, :presence => true
validates :name, :presence => true, :uniqueness => {:scope => :domains_id}
def to_s
name
end
def cname
Nic::Interface.find(nics_id).fqdn
end
end
end
Ассоциация для Nic::Base
осуществляется с помощью Concern
module ForemanCnames::Concerns::NicExtensions
extend ActiveSupport::Concern
included do
has_many :host_aliases, :foreign_key => :nics_id, :class_name => 'ForemanCnames::HostAlias', :inverse_of => :nic
accepts_nested_attributes_for :host_aliases, allow_destroy: true
end
end
Таблица foreman_cnames_host_aliases
была создана в результате миграции базы данных.
class CreateHostAliases < ActiveRecord::Migration[4.2]
def change
create_table :foreman_cnames_host_aliases do |t|
# A primary key id column is added by default
t.string :name, :limit => 255
t.references :nics, null: false, foreign_key: true
t.references :domains, null: false, foreign_key: true
t.timestamps
end
end
end
В Domain
не добавляется ассоциация, поскольку нам не нужно искать CNAME для данного домена.
Функциональность должна быть простой, как это видно по HostAlias#cname
.
irb(main):001:0> ForemanCnames::HostAlias.first.cname
=> "satellite-test-01.scc.kit.edu"
Итак, HostAlias#nic
должно быть тривиальным
def nic
Nic::Base.find(nics_id)
end
Я не могу сказать, что пытается сделать Rails, поскольку нет кода Ruby или SQL для проверки.
irb(main):001:0> ForemanCnames::HostAlias.first.method(:nic).source_location
=> ["/usr/share/gems/gems/activerecord-6.1.7/lib/active_record/associations/builder/association.rb", 102]
irb(main):002:0> ForemanCnames::HostAlias.first.nic.to_sql
Traceback (most recent call last):
2: from lib/tasks/console.rake:5:in `block in <top (required)>'
1: from (irb):2
NoMethodError (undefined method `to_sql' for nil:NilClass)
irb(main):003:0> puts Nic::Base.find(7958).host_aliases.to_sql
SELECT "foreman_cnames_host_aliases".* FROM "foreman_cnames_host_aliases" WHERE "foreman_cnames_host_aliases"."nics_id" = 7958
:inverse_of
Сначала у меня не было :inverse_of
варианта. Но, как я узнал из разных источников, это обязательно, когда установлены :class_name
и/или :accepts_nested_attributes_for
:
Тем не менее, наличие :inverse_of
или его отсутствие не меняет поведение. Вероятно, так и было бы, если бы сетевые адаптеры создавались с CNAME за один раз (это не проверялось).
Я могу проверить ассоциации через консоль Rails.
irb(main):001:0> Nic::Base.reflect_on_association(:host_aliases)
=> #<ActiveRecord::Reflection::HasManyReflection:0x000055f4135d5090 @name=:host_aliases, @scope=nil, @options = {:foreign_key=>:nics_id, :class_name=>"ForemanCnames::HostAlias", :inverse_of=>:nic, :autosave=>true}, @active_record=Nic::Base(id: integer, ...), @klass=ForemanCnames::HostAlias(id: integer, name: string, nics_id: integer, domains_id: integer), @plural_name = "host_aliases", @constructable=true, @class_name = "ForemanCnames::HostAlias", @inverse_name=:nic, @foreign_key = "nics_id", @active_record_primary_key = "id", @inverse_of=#<ActiveRecord::Reflection::BelongsToReflection:0x000055f411e906f0 @name=:nic, ...>>
irb(main):002:0> ForemanCnames::HostAlias.reflect_on_association(:nic)
=> #<ActiveRecord::Reflection::BelongsToReflection:0x000055f411e906f0 @name=:nic, @scope=nil, @options = {:class_name=>"Nic::Base", :inverse_of=>:host_aliases}, @active_record=ForemanCnames::HostAlias(id: integer, name: string, nics_id: integer, domains_id: integer), @klass=Nic::Base(id: integer, ...), @plural_name = "nics", @constructable=true, @foreign_key = "nic_id", @class_name = "Nic::Base", @inverse_name=:host_aliases, @inverse_of=#<ActiveRecord::Reflection::HasManyReflection:0x000055f4135d5090 @name=:host_aliases ...>>
irb(main):003:0> ForemanCnames::HostAlias.reflect_on_association(:domain)
=> #<ActiveRecord::Reflection::BelongsToReflection:0x000055f411e48170 @name=:domain, @scope=nil, @options = {}, @active_record=ForemanCnames::HostAlias(id: integer, name: string, nics_id: integer, domains_id: integer), @klass=nil, @plural_name = "domains", @constructable=true>
Глядя на это, мне кажется неправильным, что внешние ключи для :nic
и :domain
не имеют множественного числа.
irb(main):001:0> ForemanCnames::HostAlias.reflect_on_association(:nic).association_primary_key
=> "id"
irb(main):002:0> ForemanCnames::HostAlias.reflect_on_association(:nic).foreign_key
=> "nic_id"
irb(main):003:0> ForemanCnames::HostAlias.reflect_on_association(:domain).association_primary_key
=> "id"
irb(main):004:0> ForemanCnames::HostAlias.reflect_on_association(:domain).foreign_key
=> "domain_id"
irb(main):005:0> ForemanCnames::HostAlias
=> ForemanCnames::HostAlias(id: integer, name: string, nics_id: integer, domains_id: integer)
irb(main):006:0> ForemanCnames::HostAlias.reflect_on_association(:nic).table_name
=> "nics"
Предположительно, это произошло из частного метода ActiveRecord::Reflection::AssociationReflection#derive_foreign_key? Он берет имя ассоциации и добавляет к нему «_id», следовательно, «nic_id» и «domain_id». Хотя я не могу сказать, важно ли это.
В какой-то момент я, вероятно, захочу найти CNAME для хоста. Но это должно быть что-то похожее на has_many :host_aliases, :through => :interfaces
на Host::Base. Другими словами, :through
не является решением моей текущей проблемы.
🤔 А знаете ли вы, что...
Ruby on Rails предоставляет множество готовых решений для обработки типичных задач, таких как маршрутизация и работа с базой данных.
В соответствии с Руководством Ruby on Rails, ссылки используют имена во множественном числе при миграции базы данных, в результате чего создаются столбцы nics_id
и domains_id
. В ассоциации :belongs_to
используется вариант единственного числа и :has_many
множественного числа.
Но у Формана есть :belongs_to
связь между сетевыми адаптерами и доменами...
irb(main):006:0> Nic::Base.reflect_on_association(:domain).foreign_key
=> "domain_id"
... и ссылку на таблицу базы данных в единственном числе.
Где я ошибся?
@Макс объясняет мои настоящие ошибки в своем ответе.
Судя по всему, ссылки при создании таблицы (в виде блока) работают по-другому. Я также приму псевдоним own_to, который тогда буквально идентичен ассоциации модели.
После обновления всех вхождений "nics_id" и "domains_id"...
irb(main):001:0> n = Nic::Base.find(7958)
=> #<Nic::Managed id: 7958, ...>
irb(main):002:0> a = n.host_aliases.first
=> #<ForemanCnames::HostAlias id: 1, name: "new-alias", nic_id: 7958, domain_id: 1>
irb(main):003:0> a.nic
=> #<Nic::Managed id: 7958, ...>
irb(main):004:0> a.domain
=> #<Domain id: 1, name: "scc.kit.edu", ...>
irb(main):005:0> a.nic.fqdn
=> "satellite-test-01.scc.kit.edu"
irb(main):006:0> a.domain.name
=> "scc.kit.edu"
Вы правы в том, что плюрализация имеет значение, но вы делаете неправильные выводы. Вот соглашения об именах для столбцов внешнего ключа:
- Внешние ключи. Эти поля должны быть названы в соответствии с шаблоном сингуляризованное_имя_таблицы_id (например, item_id, order_id). Это поля, которые Active Record будет искать при создании связей между моделями.
Итак, Форман на самом деле использует правильное имя для своего столбца. Столбец внешнего ключа — это ссылка на одну строку в другой таблице. Это идентификатор одного элемента, а не всех элементов.
Судя по всему, ссылки при создании таблицы (в виде блока) работают по-другому. Я также возьму псевдоним own_to, который будет буквально идентичен ассоциации модели.
Это просто неправильно.
ссылки — это метод TableDefinition, который представляет собой просто абстракцию на основе таблицы, которая позволяет вам изменить несколько столбцов в таблице вместо методов работы с одним столбцом в SchemaStatement.
add_reference(:foos, :bar)
Становится:
change_table :foos do |t|
t.references :bar
end
Это намного менее неуклюже, если вы добавите больше столбцов в миграцию.
Они не дают разных результатов в отношении схемы. Вот как они реализованы.
def references(*args, **options)
args.each do |ref_name|
ReferenceDefinition.new(ref_name, **options).add_to(self)
end
end
def add_reference(table_name, ref_name, **options)
ReferenceDefinition.new(ref_name, **options).add(table_name, self)
end
Фактическая разница заключается в том, что add_to
добавляет изменения в определение таблицы и add
немедленно запускает SQL-запрос для изменения таблицы.
Сначала у меня не было опции :inverse_of. Но как я узнал из разных источников, это обязательно
Эти источники или ваша интерпретация неверны. Ваши ассоциации будут работать нормально даже без инверсии.
На самом деле инверсии создают связь в памяти между записями, чтобы при этом вы не теряли производительность из-за ненужного запроса к базе данных foo.bars.first.foo
. Итак, инверсии — это хорошо, но без них ваше приложение не взорвется.
Автоматические инверсии получаются из имени ассоциации, поэтому разумно указать его, если оно не может быть получено из имени ассоциации. На самом деле это не связано явно с опцией class_name
.
# Acronyms should be ALLCAPS
# setup an inflection!
module ForemanCNAMES
class HostAlias < ActiveRecord::Base
# Validations are added by default since Rails 5
belongs_to :domain
belongs_to :nic,
class_name: '::Nic::Base',
inverse_of: :host_aliases
has_one :interface, through: :nic
def to_s
name
end
def cname
interface.fqdn
end
end
end
# Do not use :: when defining classes/modules
# If you do it right and reopen the module all constants will be resolved
# from your namespace
module ForemanCNAMES
# Don't use module names and excessive nesting that provides no value
# like Concerns, Modules, Classes
module NicExtensions
extend ActiveSupport::Concern
included do
has_many :host_aliases
accepts_nested_attributes_for :host_aliases,
allow_destroy: true
end
end
end