Skip to content

Commit

Permalink
Merge pull request #20050 from lfu/add_ns_domain
Browse files Browse the repository at this point in the history
Improve automate object looking up performance.

(cherry picked from commit 6ccb2bb)
  • Loading branch information
Fryguy authored and simaishi committed May 18, 2020
1 parent 4e8d1fa commit 3fa99f5
Show file tree
Hide file tree
Showing 14 changed files with 179 additions and 82 deletions.
40 changes: 29 additions & 11 deletions app/models/miq_ae_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,28 @@ class MiqAeClass < ApplicationRecord
include MiqAeYamlImportExportMixin

belongs_to :ae_namespace, :class_name => "MiqAeNamespace", :foreign_key => :namespace_id
belongs_to :domain, :class_name => "MiqAeDomain", :inverse_of => false
has_many :ae_fields, -> { order(:priority) }, :class_name => "MiqAeField", :foreign_key => :class_id,
:dependent => :destroy, :autosave => true
:dependent => :destroy, :autosave => true, :inverse_of => :ae_class
has_many :ae_instances, -> { preload(:ae_values) }, :class_name => "MiqAeInstance", :foreign_key => :class_id,
:dependent => :destroy
:dependent => :destroy, :inverse_of => :ae_class
has_many :ae_methods, :class_name => "MiqAeMethod", :foreign_key => :class_id,
:dependent => :destroy
:dependent => :destroy, :inverse_of => :ae_class

validates_presence_of :name, :namespace_id
validates :name, :namespace_id, :domain_id, :presence => true
validates_uniqueness_of :name, :case_sensitive => false, :scope => :namespace_id
validates_format_of :name, :with => /\A[\w.-]+\z/i,
:message => N_("may contain only alphanumeric and _ . - characters")
before_validation :set_relative_path
after_save :set_children_relative_path

virtual_attribute :fqname, :string

def self.lookup_by_fqname(fqname, args = {})
ns, name = parse_fqname(fqname)
lookup_by_namespace_and_name(ns, name, args)
def self.lookup_by_fqname(fqname, _args = {})
fqname = fqname[1..-1] if fqname[0] == '/'
dname, *partial = fqname.downcase.split('/')
domain_id = MiqAeDomain.unscoped.where(MiqAeDomain.arel_table[:name].lower.eq(dname)).where(:domain_id => nil).select(:id)
where(arel_table[:relative_path].lower.eq(partial.join("/"))).find_by(:domain_id => domain_id)
end

singleton_class.send(:alias_method, :find_by_fqname, :lookup_by_fqname)
Expand Down Expand Up @@ -90,14 +95,14 @@ def to_export_xml(options = {})
end

def fqname
self.class.fqname(namespace, name)
["", domain&.name, relative_path].compact.join("/")
end

delegate :domain, :to => :ae_namespace

# my class's fqname is /domain/namespace1/namespace2/class
def namespace
return nil if ae_namespace.nil?
ae_namespace.fqname

fqname.split("/")[0..-2].join("/")
end

def namespace=(ns)
Expand Down Expand Up @@ -174,6 +179,19 @@ def self.display_name(number = 1)
n_('Automate Class', 'Automate Classes', number)
end

def set_relative_path
self.domain_id ||= domain&.id || ae_namespace&.domain_id
self.domain_id ||= ae_namespace.id if ae_namespace&.root?
self.relative_path = [ae_namespace.relative_path, name].compact.join("/") if (name_changed? || relative_path.nil?) && ae_namespace
end

def set_children_relative_path
return unless saved_change_to_relative_path?

ae_instances.each { |instance| instance.update!(:ae_class => self, :relative_path => nil) }
ae_methods.each { |method| method.update!(:ae_class => self, :relative_path => nil) }
end

private

def self.sub_namespaces(ns_obj, ids)
Expand Down
3 changes: 2 additions & 1 deletion app/models/miq_ae_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ class MiqAeField < ApplicationRecord

belongs_to :ae_class, :class_name => "MiqAeClass", :foreign_key => :class_id, :touch => true
belongs_to :ae_method, :class_name => "MiqAeMethod", :foreign_key => :method_id, :touch => true
has_many :ae_values, :class_name => "MiqAeValue", :foreign_key => :field_id, :dependent => :destroy
has_many :ae_values, :class_name => "MiqAeValue", :foreign_key => :field_id, :dependent => :destroy,
:inverse_of => :ae_field

validates_uniqueness_of :name, :case_sensitive => false, :scope => [:class_id, :method_id]
validates_presence_of :name
Expand Down
28 changes: 24 additions & 4 deletions app/models/miq_ae_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ class MiqAeInstance < ApplicationRecord
include MiqAeSetUserInfoMixin
include MiqAeYamlImportExportMixin

belongs_to :domain, :class_name => "MiqAeDomain", :inverse_of => false
belongs_to :ae_class, -> { includes(:ae_fields) }, :class_name => "MiqAeClass", :foreign_key => :class_id
has_many :ae_values, -> { includes(:ae_field) }, :class_name => "MiqAeValue", :foreign_key => :instance_id,
:dependent => :destroy, :autosave => true

before_validation :set_relative_path
validates_uniqueness_of :name, :case_sensitive => false, :scope => :class_id
validates_presence_of :name
validates :name, :domain_id, :class_id, :presence => true
validates_format_of :name, :with => /\A[\w.-]+\z/i,
:message => N_("may contain only alphanumeric and _ . - characters")

Expand Down Expand Up @@ -63,10 +65,13 @@ def field_attributes
end

def fqname
"#{ae_class.fqname}/#{name}"
["", domain&.name, relative_path].compact.join("/")
end

delegate :domain, :to => :ae_class
# my instance's fqname is /domain/namespace1/namespace2/class/instance
def namespace
fqname.split("/")[0..-3].join("/")
end

def export_ae_fields
ae_values_sorted.collect(&:to_export_yaml).compact
Expand All @@ -75,7 +80,11 @@ def export_ae_fields
# TODO: Limit search to within the context of a class id?
def self.search(str)
str[-1, 1] = "%" if str[-1, 1] == "*"
where("lower(name) LIKE ?", str.downcase).pluck(:name)

query = where(arel_table[:relative_path].lower.matches(str.downcase, nil, true)) # This uses 'like'.

domain_id = query.joins(:domain).order("miq_ae_namespaces.priority DESC").limit(1).pluck(:domain_id)
query.where(:domain_id => domain_id)
end

def to_export_xml(options = {})
Expand Down Expand Up @@ -139,6 +148,12 @@ def self.display_name(number = 1)
n_('Automate Instance', 'Automate Instances', number)
end

def self.find_best_match_by(user, relative_path)
domain_ids = user.current_tenant.enabled_domains.collect(&:id)
joins(:domain).where(:miq_ae_namespaces => {:id => domain_ids}).order("miq_ae_namespaces.priority DESC")
.find_by(arel_table[:relative_path].lower.matches(relative_path.downcase))
end

def self.get_homonymic_across_domains(user, fqname, enabled = nil, prefix: true)
return get_homonymic_across_domains_noprefix(user, fqname, enabled) unless prefix

Expand All @@ -152,6 +167,11 @@ def self.get_homonymic_across_domains(user, fqname, enabled = nil, prefix: true)
MiqAeDatastore.get_sorted_matching_objects(user, ::MiqAeInstance, ns, klass, instance, enabled)
end

def set_relative_path
self.domain_id ||= ae_class&.domain_id
self.relative_path = "#{ae_class.relative_path}/#{name}" if (name_changed? || relative_path_changed?) && ae_class
end

private

def validate_field(field)
Expand Down
22 changes: 19 additions & 3 deletions app/models/miq_ae_method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ class MiqAeMethod < ApplicationRecord
default_value_for(:embedded_methods) { [] }
validates :embedded_methods, :exclusion => { :in => [nil] }
serialize :options, Hash
before_validation :set_relative_path

belongs_to :domain, :class_name => "MiqAeDomain", :inverse_of => false
belongs_to :ae_class, :class_name => "MiqAeClass", :foreign_key => :class_id
has_many :inputs, -> { order(:priority) }, :class_name => "MiqAeField", :foreign_key => :method_id,
:dependent => :destroy, :autosave => true

validates_presence_of :name, :scope
validates :name, :scope, :domain_id, :class_id, :presence => true
validates_uniqueness_of :name, :case_sensitive => false, :scope => [:class_id, :scope]
validates_format_of :name, :with => /\A[\w]+\z/i,
:message => N_("may contain only alphanumeric and _ characters")
Expand Down Expand Up @@ -47,10 +49,13 @@ def self.validate_syntax(code_text)
end

def fqname
"#{ae_class.fqname}/#{name}"
["", domain&.name, relative_path].compact.join("/")
end

delegate :domain, :to => :ae_class
# my method's fqname is /domain/namespace1/namespace2/class/method
def namespace
fqname.split("/")[0..-3].join("/")
end

def self.default_method_text
<<-DEFAULT_METHOD_TEXT
Expand Down Expand Up @@ -136,4 +141,15 @@ def self.lookup_by_class_id_and_name(class_id, name)
def self.display_name(number = 1)
n_('Automate Method', 'Automate Methods', number)
end

def self.find_best_match_by(user, relative_path)
domain_ids = user.current_tenant.enabled_domains.collect(&:id)
joins(:domain).where(:miq_ae_namespaces => {:id => domain_ids}).order("miq_ae_namespaces.priority DESC")
.find_by(arel_table[:relative_path].lower.matches(relative_path.downcase))
end

def set_relative_path
self.domain_id ||= ae_class&.domain_id
self.relative_path = "#{ae_class.relative_path}/#{name}" if (name_changed? || relative_path_changed?) && ae_class
end
end
54 changes: 37 additions & 17 deletions app/models/miq_ae_namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,38 @@ class MiqAeNamespace < ApplicationRecord
/^commit_time/, /^commit_sha/, /^ref$/, /^ref_type$/,
/^last_import_on/, /^source/, /^top_level_namespace/].freeze

belongs_to :domain, :class_name => "MiqAeDomain", :inverse_of => false
has_many :ae_classes, -> { includes([:ae_methods, :ae_fields, :ae_instances]) }, :class_name => "MiqAeClass",
:foreign_key => :namespace_id, :dependent => :destroy, :inverse_of => false
:foreign_key => :namespace_id, :dependent => :destroy, :inverse_of => :ae_namespace

validates :name,
:format => {:with => /\A[\w\.\-\$]+\z/i, :message => N_("may contain only alphanumeric and _ . - $ characters")},
:presence => true,
:uniqueness => {:scope => :ancestry, :case_sensitive => false}

alias ae_namespaces children
alias_attribute :fqname_sans_domain, :relative_path
virtual_has_many :ae_namespaces
alias ae_namespaces children

before_validation :set_relative_path
after_save :set_children_relative_path

def parent
parent_id == domain_id ? domain : super
end

def self.lookup_by_fqname(fqname, include_classes = true)
return nil if fqname.blank?

fqname = fqname[0] == '/' ? fqname : "/#{fqname}"
fqname = fqname.downcase
last = fqname.split('/').last
low_name = arel_table[:name].lower
fqname = fqname[1..-1] if fqname[0] == '/'
dname, *partial = fqname.downcase.split('/')

domain_query = MiqAeDomain.unscoped.where(MiqAeDomain.arel_table[:name].lower.eq(dname)).where(:domain_id => nil)
return domain_query.first if partial.empty?

query = include_classes ? includes(:ae_classes) : all
query.where(low_name.eq(last)).detect { |namespace| namespace.fqname.downcase == fqname }
query = query.where(arel_table[:relative_path].lower.eq(partial.join("/")))
query.find_by(:domain_id => domain_query.select(:id))
end

singleton_class.send(:alias_method, :find_by_fqname, :lookup_by_fqname)
Expand Down Expand Up @@ -61,6 +72,7 @@ def self.find_or_create_by_fqname(fqname, include_classes = true)
found
end

# TODO: broken since 2017
def self.find_tree(find_options = {})
namespaces = where(find_options)
ns_lookup = namespaces.inject({}) do |h, ns|
Expand Down Expand Up @@ -88,7 +100,9 @@ def self.find_tree(find_options = {})
end

def fqname
@fqname ||= "/#{path.pluck(:name).join('/')}"
return "/#{name}" if domain_id.blank?

["", domain&.name, relative_path].compact.join("/")
end

def editable?(user = User.current_user)
Expand All @@ -103,16 +117,8 @@ def ns_fqname
fqname.sub(domain_name.to_s, '')
end

def fqname_sans_domain
fqname.split('/')[2..-1].join("/")
end

def domain_name
domain.try(:name)
end

def domain
root if root.domain?
domain&.name
end

def domain?
Expand All @@ -122,4 +128,18 @@ def domain?
def self.display_name(number = 1)
n_('Automate Namespace', 'Automate Namespaces', number)
end

def set_relative_path
return if root?

self.domain_id ||= parent.domain_id || parent.id
self.relative_path = [parent.relative_path, name].compact.join("/") if name_changed? || relative_path.nil?
end

def set_children_relative_path
return unless saved_change_to_relative_path?

ae_namespaces.each { |ns| ns.update!(:parent => self, :relative_path => nil) }
ae_classes.each { |klass| klass.update!(:ae_namespace => self, :relative_path => nil) }
end
end
20 changes: 7 additions & 13 deletions spec/factories/miq_ae_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

trait :with_instances_and_methods do
transient do
ae_fields {}
ae_instances {}
ae_methods {}
ae_fields { {} }
ae_instances { {} }
ae_methods { {} }
end

after :create do |aeclass, evaluator|
Expand All @@ -15,28 +15,22 @@
end

evaluator.ae_instances.each do |name, values|
FactoryBot.create(:miq_ae_instance, :with_values,
FactoryBot.create(:miq_ae_instance,
:class_id => aeclass.id,
:name => name,
'values' => values)
end

evaluator.ae_methods.each do |name, aemethod|
FactoryBot.create(:miq_ae_method, :with_params,
FactoryBot.create(:miq_ae_method,
{:class_id => aeclass.id,
:name => name}.merge(aemethod))
end
end
end

trait :of_domain do
transient do
domain { nil }
end

before(:create) do |ae_class, evaluator|
ae_class.namespace_id = FactoryBot.create(:miq_ae_namespace, :parent => evaluator.domain).id
end
before(:create) do |aeclass|
aeclass.ae_namespace ||= FactoryBot.create(:miq_ae_namespace, :parent => aeclass.domain) unless aeclass.namespace_id
end
end
end
2 changes: 1 addition & 1 deletion spec/factories/miq_ae_domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
git_repository { FactoryBot.create(:git_repository) }
end

factory :miq_ae_domain, :parent => :miq_ae_namespace, :class => "MiqAeDomain" do
factory :miq_ae_domain, :class => "MiqAeDomain" do
sequence(:name) { |n| "miq_ae_domain#{seq_padded_for_sorting(n)}" }
tenant { Tenant.seed }
enabled { true }
Expand Down
14 changes: 9 additions & 5 deletions spec/factories/miq_ae_instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
factory :miq_ae_instance do
sequence(:name) { |n| "miq_ae_instance_#{seq_padded_for_sorting(n)}" }

trait :with_values do
transient do
values {}
end
transient do
values { {} }
end

before :create do |aeinstance, evaluator|
aeinstance.ae_class ||= FactoryBot.create(:miq_ae_class) unless evaluator.class_id
end

after :create do |aeinstance, evaluator|
after :create do |aeinstance, evaluator|
unless evaluator.values.empty?
aeinstance.ae_values << aeinstance.ae_class.ae_fields.collect do |field|
next unless evaluator.values.key?(field.name)
FactoryBot.build(:miq_ae_value, {:field_id => field.id}.merge(evaluator.values[field.name]))
Expand Down
Loading

0 comments on commit 3fa99f5

Please sign in to comment.