##// END OF EJS Templates
Reduces the number of subqueries when searching with many custom fields set as searchable (#15781)....
Jean-Philippe Lang -
r12206:77ac3b7891d4
parent child
Show More
@@ -1,280 +1,281
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomField < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 has_many :custom_values, :dependent => :delete_all
22 22 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
23 23 acts_as_list :scope => 'type = \'#{self.class}\''
24 24 serialize :possible_values
25 25 store :format_store
26 26
27 27 validates_presence_of :name, :field_format
28 28 validates_uniqueness_of :name, :scope => :type
29 29 validates_length_of :name, :maximum => 30
30 30 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
31 31 validate :validate_custom_field
32 32
33 33 before_validation :set_searchable
34 34 before_save do |field|
35 35 field.format.before_custom_field_save(field)
36 36 end
37 37 after_save :handle_multiplicity_change
38 38 after_save do |field|
39 39 if field.visible_changed? && field.visible
40 40 field.roles.clear
41 41 end
42 42 end
43 43
44 44 scope :sorted, lambda { order("#{table_name}.position ASC") }
45 45 scope :visible, lambda {|*args|
46 46 user = args.shift || User.current
47 47 if user.admin?
48 48 # nop
49 49 elsif user.memberships.any?
50 50 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
51 51 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
52 52 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
53 53 " WHERE m.user_id = ?)",
54 54 true, user.id)
55 55 else
56 56 where(:visible => true)
57 57 end
58 58 }
59 59
60 60 def visible_by?(project, user=User.current)
61 61 visible? || user.admin?
62 62 end
63 63
64 64 def format
65 65 @format ||= Redmine::FieldFormat.find(field_format)
66 66 end
67 67
68 68 def field_format=(arg)
69 69 # cannot change format of a saved custom field
70 70 if new_record?
71 71 @format = nil
72 72 super
73 73 end
74 74 end
75 75
76 76 def set_searchable
77 77 # make sure these fields are not searchable
78 78 self.searchable = false unless format.class.searchable_supported
79 79 # make sure only these fields can have multiple values
80 80 self.multiple = false unless format.class.multiple_supported
81 81 true
82 82 end
83 83
84 84 def validate_custom_field
85 85 format.validate_custom_field(self).each do |attribute, message|
86 86 errors.add attribute, message
87 87 end
88 88
89 89 if regexp.present?
90 90 begin
91 91 Regexp.new(regexp)
92 92 rescue
93 93 errors.add(:regexp, :invalid)
94 94 end
95 95 end
96 96
97 97 if default_value.present?
98 98 validate_field_value(default_value).each do |message|
99 99 errors.add :default_value, message
100 100 end
101 101 end
102 102 end
103 103
104 104 def possible_custom_value_options(custom_value)
105 105 format.possible_custom_value_options(custom_value)
106 106 end
107 107
108 108 def possible_values_options(object=nil)
109 109 if object.is_a?(Array)
110 110 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
111 111 else
112 112 format.possible_values_options(self, object) || []
113 113 end
114 114 end
115 115
116 116 def possible_values
117 117 values = super()
118 118 if values.is_a?(Array)
119 119 values.each do |value|
120 120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
121 121 end
122 122 values
123 123 else
124 124 []
125 125 end
126 126 end
127 127
128 128 # Makes possible_values accept a multiline string
129 129 def possible_values=(arg)
130 130 if arg.is_a?(Array)
131 131 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
132 132 else
133 133 self.possible_values = arg.to_s.split(/[\n\r]+/)
134 134 end
135 135 end
136 136
137 137 def cast_value(value)
138 138 format.cast_value(self, value)
139 139 end
140 140
141 141 def value_from_keyword(keyword, customized)
142 142 possible_values_options = possible_values_options(customized)
143 143 if possible_values_options.present?
144 144 keyword = keyword.to_s.downcase
145 145 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
146 146 if v.is_a?(Array)
147 147 v.last
148 148 else
149 149 v
150 150 end
151 151 end
152 152 else
153 153 keyword
154 154 end
155 155 end
156 156
157 157 # Returns a ORDER BY clause that can used to sort customized
158 158 # objects by their value of the custom field.
159 159 # Returns nil if the custom field can not be used for sorting.
160 160 def order_statement
161 161 return nil if multiple?
162 162 format.order_statement(self)
163 163 end
164 164
165 165 # Returns a GROUP BY clause that can used to group by custom value
166 166 # Returns nil if the custom field can not be used for grouping.
167 167 def group_statement
168 168 return nil if multiple?
169 169 format.group_statement(self)
170 170 end
171 171
172 172 def join_for_order_statement
173 173 format.join_for_order_statement(self)
174 174 end
175 175
176 def visibility_by_project_condition(project_key=nil, user=User.current)
176 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
177 177 if visible? || user.admin?
178 178 "1=1"
179 179 elsif user.anonymous?
180 180 "1=0"
181 181 else
182 182 project_key ||= "#{self.class.customized_class.table_name}.project_id"
183 id_column ||= id
183 184 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
184 185 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
185 186 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
186 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
187 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
187 188 end
188 189 end
189 190
190 191 def self.visibility_condition
191 192 if user.admin?
192 193 "1=1"
193 194 elsif user.anonymous?
194 195 "#{table_name}.visible"
195 196 else
196 197 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
197 198 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
198 199 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
199 200 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
200 201 end
201 202 end
202 203
203 204 def <=>(field)
204 205 position <=> field.position
205 206 end
206 207
207 208 # Returns the class that values represent
208 209 def value_class
209 210 format.target_class if format.respond_to?(:target_class)
210 211 end
211 212
212 213 def self.customized_class
213 214 self.name =~ /^(.+)CustomField$/
214 215 $1.constantize rescue nil
215 216 end
216 217
217 218 # to move in project_custom_field
218 219 def self.for_all
219 220 where(:is_for_all => true).order('position').all
220 221 end
221 222
222 223 def type_name
223 224 nil
224 225 end
225 226
226 227 # Returns the error messages for the given value
227 228 # or an empty array if value is a valid value for the custom field
228 229 def validate_custom_value(custom_value)
229 230 value = custom_value.value
230 231 errs = []
231 232 if value.is_a?(Array)
232 233 if !multiple?
233 234 errs << ::I18n.t('activerecord.errors.messages.invalid')
234 235 end
235 236 if is_required? && value.detect(&:present?).nil?
236 237 errs << ::I18n.t('activerecord.errors.messages.blank')
237 238 end
238 239 else
239 240 if is_required? && value.blank?
240 241 errs << ::I18n.t('activerecord.errors.messages.blank')
241 242 end
242 243 end
243 244 if custom_value.value.present?
244 245 errs += format.validate_custom_value(custom_value)
245 246 end
246 247 errs
247 248 end
248 249
249 250 # Returns the error messages for the default custom field value
250 251 def validate_field_value(value)
251 252 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
252 253 end
253 254
254 255 # Returns true if value is a valid value for the custom field
255 256 def valid_field_value?(value)
256 257 validate_field_value(value).empty?
257 258 end
258 259
259 260 def format_in?(*args)
260 261 args.include?(field_format)
261 262 end
262 263
263 264 protected
264 265
265 266 # Removes multiple values for the custom field after setting the multiple attribute to false
266 267 # We kepp the value with the highest id for each customized object
267 268 def handle_multiplicity_change
268 269 if !new_record? && multiple_was && !multiple
269 270 ids = custom_values.
270 271 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
271 272 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
272 273 " AND cve.id > #{CustomValue.table_name}.id)").
273 274 pluck(:id)
274 275
275 276 if ids.any?
276 277 custom_values.where(:id => ids).delete_all
277 278 end
278 279 end
279 280 end
280 281 end
@@ -1,44 +1,45
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssueCustomField < CustomField
19 19 has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
20 20 has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
21 21 has_many :issues, :through => :issue_custom_values
22 22
23 23 def type_name
24 24 :label_issue_plural
25 25 end
26 26
27 27 def visible_by?(project, user=User.current)
28 28 super || (roles & user.roles_for_project(project)).present?
29 29 end
30 30
31 def visibility_by_project_condition(*args)
31 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
32 32 sql = super
33 additional_sql = "#{Issue.table_name}.tracker_id IN (SELECT tracker_id FROM #{table_name_prefix}custom_fields_trackers#{table_name_suffix} WHERE custom_field_id = #{id})"
34 unless is_for_all?
35 additional_sql << " AND #{Issue.table_name}.project_id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id})"
36 end
37 "((#{sql}) AND (#{additional_sql}))"
33 id_column ||= id
34 tracker_condition = "#{Issue.table_name}.tracker_id IN (SELECT tracker_id FROM #{table_name_prefix}custom_fields_trackers#{table_name_suffix} WHERE custom_field_id = #{id_column})"
35 project_condition = "EXISTS (SELECT 1 FROM #{CustomField.table_name} ifa WHERE ifa.is_for_all = #{connection.quoted_true} AND ifa.id = #{id_column})" +
36 " OR #{Issue.table_name}.project_id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id_column})"
37
38 "((#{sql}) AND (#{tracker_condition}) AND (#{project_condition}))"
38 39 end
39 40
40 41 def validate_custom_field
41 42 super
42 43 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present?
43 44 end
44 45 end
@@ -1,135 +1,140
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module Acts
20 20 module Searchable
21 21 def self.included(base)
22 22 base.extend ClassMethods
23 23 end
24 24
25 25 module ClassMethods
26 26 # Options:
27 27 # * :columns - a column or an array of columns to search
28 28 # * :project_key - project foreign key (default to project_id)
29 29 # * :date_column - name of the datetime column (default to created_on)
30 30 # * :sort_order - name of the column used to sort results (default to :date_column or created_on)
31 31 # * :permission - permission required to search the model (default to :view_"objects")
32 32 def acts_as_searchable(options = {})
33 33 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
34 34
35 35 cattr_accessor :searchable_options
36 36 self.searchable_options = options
37 37
38 38 if searchable_options[:columns].nil?
39 39 raise 'No searchable column defined.'
40 40 elsif !searchable_options[:columns].is_a?(Array)
41 41 searchable_options[:columns] = [] << searchable_options[:columns]
42 42 end
43 43
44 44 searchable_options[:project_key] ||= "#{table_name}.project_id"
45 45 searchable_options[:date_column] ||= "#{table_name}.created_on"
46 46 searchable_options[:order_column] ||= searchable_options[:date_column]
47 47
48 48 # Should we search custom fields on this model ?
49 49 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
50 50
51 51 send :include, Redmine::Acts::Searchable::InstanceMethods
52 52 end
53 53 end
54 54
55 55 module InstanceMethods
56 56 def self.included(base)
57 57 base.extend ClassMethods
58 58 end
59 59
60 60 module ClassMethods
61 61 # Searches the model for the given tokens
62 62 # projects argument can be either nil (will search all projects), a project or an array of projects
63 63 # Returns the results and the results count
64 64 def search(tokens, projects=nil, options={})
65 65 if projects.is_a?(Array) && projects.empty?
66 66 # no results
67 67 return [[], 0]
68 68 end
69 69
70 70 # TODO: make user an argument
71 71 user = User.current
72 72 tokens = [] << tokens unless tokens.is_a?(Array)
73 73 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
74 74
75 75 limit_options = {}
76 76 limit_options[:limit] = options[:limit] if options[:limit]
77 77
78 78 columns = searchable_options[:columns]
79 79 columns = columns[0..0] if options[:titles_only]
80 80
81 81 token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
82 82
83 83 if !options[:titles_only] && searchable_options[:search_custom_fields]
84 84 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
85 searchable_custom_fields.each do |field|
86 sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
87 " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
88 " AND #{CustomValue.table_name}.custom_field_id = #{field.id})" +
89 " AND #{field.visibility_by_project_condition(searchable_options[:project_key], user)}"
85 fields_by_visibility = searchable_custom_fields.group_by {|field|
86 field.visibility_by_project_condition(searchable_options[:project_key], user, "cfs.custom_field_id")
87 }
88 # only 1 subquery for all custom fields with the same visibility statement
89 fields_by_visibility.each do |visibility, fields|
90 ids = fields.map(&:id).join(',')
91 sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" +
92 " WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id AND LOWER(cfs.value) LIKE ?" +
93 " AND cfs.custom_field_id IN (#{ids})" +
94 " AND #{visibility})"
90 95 token_clauses << sql
91 96 end
92 97 end
93 98
94 99 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
95 100
96 101 tokens_conditions = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
97 102
98 103 scope = self.scoped
99 104 project_conditions = []
100 105 if searchable_options.has_key?(:permission)
101 106 project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
102 107 elsif respond_to?(:visible)
103 108 scope = scope.visible(user)
104 109 else
105 110 ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
106 111 project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
107 112 end
108 113 # TODO: use visible scope options instead
109 114 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
110 115 project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
111 116
112 117 results = []
113 118 results_count = 0
114 119
115 120 scope = scope.
116 121 includes(searchable_options[:include]).
117 122 order("#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')).
118 123 where(project_conditions).
119 124 where(tokens_conditions)
120 125
121 126 results_count = scope.count
122 127
123 128 scope_with_limit = scope.limit(options[:limit])
124 129 if options[:offset]
125 130 scope_with_limit = scope_with_limit.where("#{searchable_options[:date_column]} #{options[:before] ? '<' : '>'} ?", options[:offset])
126 131 end
127 132 results = scope_with_limit.all
128 133
129 134 [results, results_count]
130 135 end
131 136 end
132 137 end
133 138 end
134 139 end
135 140 end
General Comments 0
You need to be logged in to leave comments. Login now