nobrainer 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_brainer/config.rb +9 -16
  3. data/lib/no_brainer/connection.rb +32 -26
  4. data/lib/no_brainer/criteria/chainable/core.rb +17 -20
  5. data/lib/no_brainer/criteria/chainable/limit.rb +2 -2
  6. data/lib/no_brainer/criteria/chainable/order_by.rb +24 -7
  7. data/lib/no_brainer/criteria/chainable/raw.rb +11 -3
  8. data/lib/no_brainer/criteria/chainable/scope.rb +11 -7
  9. data/lib/no_brainer/criteria/chainable/where.rb +32 -15
  10. data/lib/no_brainer/criteria/termination/cache.rb +13 -9
  11. data/lib/no_brainer/criteria/termination/eager_loading.rb +12 -26
  12. data/lib/no_brainer/document.rb +2 -6
  13. data/lib/no_brainer/document/association.rb +8 -7
  14. data/lib/no_brainer/document/association/belongs_to.rb +27 -22
  15. data/lib/no_brainer/document/association/core.rb +8 -8
  16. data/lib/no_brainer/document/association/eager_loader.rb +57 -0
  17. data/lib/no_brainer/document/association/has_many.rb +49 -26
  18. data/lib/no_brainer/document/association/has_many_through.rb +31 -0
  19. data/lib/no_brainer/document/association/has_one.rb +13 -0
  20. data/lib/no_brainer/document/association/has_one_through.rb +10 -0
  21. data/lib/no_brainer/document/attributes.rb +4 -3
  22. data/lib/no_brainer/document/core.rb +1 -1
  23. data/lib/no_brainer/document/criteria.rb +2 -3
  24. data/lib/no_brainer/document/index.rb +3 -3
  25. data/lib/no_brainer/document/polymorphic.rb +1 -1
  26. data/lib/no_brainer/document/serialization.rb +1 -1
  27. data/lib/no_brainer/document/store_in.rb +5 -3
  28. data/lib/no_brainer/error.rb +1 -0
  29. data/lib/no_brainer/query_runner.rb +2 -1
  30. data/lib/no_brainer/query_runner/driver.rb +1 -4
  31. data/lib/no_brainer/query_runner/missing_index.rb +11 -0
  32. data/lib/no_brainer/query_runner/run_options.rb +2 -3
  33. data/lib/no_brainer/railtie.rb +0 -1
  34. data/lib/no_brainer/version.rb +1 -1
  35. data/lib/nobrainer.rb +5 -5
  36. metadata +28 -24
  37. data/lib/no_brainer/database.rb +0 -41
@@ -17,21 +17,25 @@ module NoBrainer::Criteria::Termination::Cache
17
17
  msg
18
18
  end
19
19
 
20
- def merge!(criteria)
20
+ def merge!(criteria, options={})
21
21
  super
22
22
  self._with_cache = criteria._with_cache unless criteria._with_cache.nil?
23
- self.reload
23
+ self.reload unless options[:keep_cache]
24
24
  self
25
25
  end
26
26
 
27
27
  def with_cache?
28
- @_with_cache.nil? ? NoBrainer::Config.cache_documents : @_with_cache
28
+ @_with_cache != false
29
29
  end
30
30
 
31
31
  def reload
32
32
  @cache = nil
33
33
  end
34
34
 
35
+ def cached?
36
+ !!@cache
37
+ end
38
+
35
39
  def each(options={}, &block)
36
40
  return super unless with_cache? && !options[:no_cache] && block
37
41
  return @cache.each(&block) if @cache
@@ -41,7 +45,7 @@ module NoBrainer::Criteria::Termination::Cache
41
45
  block.call(instance)
42
46
  cache << instance
43
47
  end
44
- @cache = cache
48
+ @cache = cache.freeze
45
49
  self
46
50
  end
47
51
 
@@ -49,19 +53,19 @@ module NoBrainer::Criteria::Termination::Cache
49
53
  @cache = cache
50
54
  end
51
55
 
52
- def self.reload_on(*methods)
56
+ def self.use_cache_for(*methods)
53
57
  methods.each do |method|
54
58
  define_method(method) do |*args, &block|
55
- reload
56
- super(*args, &block).tap { reload }
59
+ @cache ? @cache.__send__(method, *args, &block) : super(*args, &block)
57
60
  end
58
61
  end
59
62
  end
60
63
 
61
- def self.use_cache_for(*methods)
64
+ def self.reload_on(*methods)
62
65
  methods.each do |method|
63
66
  define_method(method) do |*args, &block|
64
- @cache ? @cache.__send__(method, *args, &block) : super(*args, &block)
67
+ reload
68
+ super(*args, &block).tap { reload }
65
69
  end
66
70
  end
67
71
  end
@@ -9,13 +9,17 @@ module NoBrainer::Criteria::Termination::EagerLoading
9
9
  end
10
10
 
11
11
  def includes(*values)
12
- raise "Please enable caching with NoBrainer::Config.cache_documents = true" unless NoBrainer::Config.cache_documents
13
- chain { |criteria| criteria._includes = values }
12
+ chain(:keep_cache => true) { |criteria| criteria._includes = values }
14
13
  end
15
14
 
16
- def merge!(criteria)
15
+ def merge!(criteria, options={})
17
16
  super
18
17
  self._includes = self._includes + criteria._includes
18
+
19
+ # XXX Not pretty hack
20
+ if criteria._includes.present? && cached?
21
+ perform_eager_loading(@cache)
22
+ end
19
23
  end
20
24
 
21
25
  def each(options={}, &block)
@@ -23,7 +27,7 @@ module NoBrainer::Criteria::Termination::EagerLoading
23
27
 
24
28
  docs = []
25
29
  super(options.merge(:no_eager_loading => true)) { |doc| docs << doc }
26
- eager_load(docs, self._includes)
30
+ perform_eager_loading(docs)
27
31
  docs.each(&block)
28
32
  self
29
33
  end
@@ -35,30 +39,12 @@ module NoBrainer::Criteria::Termination::EagerLoading
35
39
  end
36
40
 
37
41
  def get_one(criteria)
38
- super.tap { |doc| eager_load([doc], self._includes) if should_eager_load? }
42
+ super.tap { |doc| perform_eager_loading([doc]) }
39
43
  end
40
44
 
41
- def eager_load_association(docs, association_name, criteria=nil)
42
- docs = docs.compact
43
- return if docs.empty?
44
- association = docs.first.root_class.association_metadata[association_name.to_sym]
45
- raise "Unknown association #{association_name}" unless association
46
- association.eager_load(docs, criteria)
47
- end
48
-
49
- def eager_load(docs, includes)
50
- case includes
51
- when Hash then includes.each do |k,v|
52
- if v.is_a?(NoBrainer::Criteria)
53
- v = v.dup
54
- nested_includes, v._includes = v._includes, []
55
- eager_load(eager_load_association(docs, k, v), nested_includes)
56
- else
57
- eager_load(eager_load_association(docs, k), v)
58
- end
59
- end
60
- when Array then includes.each { |v| eager_load(docs, v) }
61
- else eager_load_association(docs, includes)
45
+ def perform_eager_loading(docs)
46
+ if should_eager_load? && docs.present?
47
+ NoBrainer::Document::Association::EagerLoader.new.eager_load(docs, self._includes)
62
48
  end
63
49
  end
64
50
  end
@@ -6,13 +6,9 @@ module NoBrainer::Document
6
6
 
7
7
  autoload_and_include :Core, :StoreIn, :InjectionLayer, :Attributes, :Persistance, :Dirty,
8
8
  :Id, :Association, :Serialization, :Criteria, :Validation,
9
- :Polymorphic, :Index
9
+ :Polymorphic, :Index, :Timestamps
10
10
 
11
- autoload :DynamicAttributes, :Timestamps
12
-
13
- included do
14
- include Timestamps if NoBrainer::Config.auto_include_timestamps
15
- end
11
+ autoload :DynamicAttributes
16
12
 
17
13
  singleton_class.delegate :all, :to => Core
18
14
  end
@@ -1,17 +1,16 @@
1
1
  module NoBrainer::Document::Association
2
2
  extend NoBrainer::Autoload
3
- autoload :Core, :BelongsTo, :HasMany
3
+ autoload :Core, :BelongsTo, :HasMany, :HasManyThrough, :HasOne, :HasOneThrough, :EagerLoader
4
4
 
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- class << self; attr_accessor :association_metadata; end
8
+ singleton_class.send(:attr_accessor, :association_metadata)
9
9
  self.association_metadata = {}
10
10
  end
11
11
 
12
- def association(metadata)
13
- @associations ||= {}
14
- @associations[metadata] ||= metadata.new(self)
12
+ def associations
13
+ @associations ||= Hash.new { |h, metadata| h[metadata] = metadata.new(self) }
15
14
  end
16
15
 
17
16
  module ClassMethods
@@ -20,14 +19,16 @@ module NoBrainer::Document::Association
20
19
  subclass.association_metadata = self.association_metadata.dup
21
20
  end
22
21
 
23
- [:belongs_to, :has_many].each do |association|
22
+ [:belongs_to, :has_many, :has_one].each do |association|
24
23
  define_method(association) do |target, options={}|
25
24
  target = target.to_sym
26
25
 
27
26
  if r = self.association_metadata[target]
27
+ raise "Cannot change the :through option" unless r.options[:through] == options[:through]
28
28
  r.options.merge!(options)
29
29
  else
30
- metadata_klass = NoBrainer::Document::Association.const_get(association.to_s.camelize).const_get(:Metadata)
30
+ klass_name = (options[:through] ? "#{association}_through" : association.to_s).camelize
31
+ metadata_klass = NoBrainer::Document::Association.const_get(klass_name).const_get(:Metadata)
31
32
  r = metadata_klass.new(self, target, options)
32
33
  ([self] + descendants).each do |klass|
33
34
  klass.association_metadata[target] = r
@@ -2,8 +2,9 @@ class NoBrainer::Document::Association::BelongsTo
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_BELONGS_TO_OPTIONS = [:foreign_key, :class_name, :index]
5
+ VALID_OPTIONS = [:foreign_key, :class_name, :index]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
+ extend NoBrainer::Document::Association::EagerLoader::Generic
7
8
 
8
9
  def foreign_key
9
10
  # TODO test :foreign_key
@@ -17,47 +18,51 @@ class NoBrainer::Document::Association::BelongsTo
17
18
 
18
19
  def hook
19
20
  super
20
- options.assert_valid_keys(*VALID_BELONGS_TO_OPTIONS)
21
21
 
22
22
  owner_klass.field foreign_key, :index => options[:index]
23
23
  delegate("#{foreign_key}=", :assign_foreign_key, :call_super => true)
24
24
  add_callback_for(:after_validation)
25
+ # TODO test if we are not overstepping on another foreign_key
25
26
  end
26
27
 
27
- def eager_load(docs, criteria=nil)
28
- target_criteria = target_klass.all
29
- target_criteria = target_criteria.merge(criteria) if criteria
30
- docs_fks = Hash[docs.map { |doc| [doc, doc.read_attribute(foreign_key)] }]
31
- fks = docs_fks.values.compact.uniq
32
- fk_targets = Hash[target_criteria.where(:id.in => fks).map { |doc| [doc.id, doc] }]
33
- docs_fks.each { |doc, fk| doc.association(self)._write(fk_targets[fk]) if fk_targets[fk] }
34
- fk_targets.values
35
- end
28
+ eager_load_with :owner_key => ->{ foreign_key }, :target_key => ->{ :id },
29
+ :unscoped => true
36
30
  end
37
31
 
32
+ # Note:
33
+ # @target_container is an array to distinguish the following cases:
34
+ # * target is not loaded, but perhaps present in the db.
35
+ # * we already tried to load target, but it wasn't present in the db.
36
+
38
37
  def assign_foreign_key(value)
39
- @target = nil
38
+ @target_container = nil
40
39
  end
41
40
 
42
41
  def read
43
- return @target if @target && NoBrainer::Config.cache_documents
44
- if fk = instance.read_attribute(foreign_key)
45
- @target = target_klass.find(fk)
46
- end
47
- end
42
+ return @target_container.first if loaded?
48
43
 
49
- def _write(target)
50
- @target = target
44
+ if fk = owner.read_attribute(foreign_key)
45
+ preload(target_klass.find(fk))
46
+ end
51
47
  end
52
48
 
53
49
  def write(target)
54
50
  assert_target_type(target)
55
- instance.write_attribute(foreign_key, target.try(:id))
56
- _write(target)
51
+ owner.write_attribute(foreign_key, target.try(:id))
52
+ preload(target)
53
+ end
54
+
55
+ def preload(target)
56
+ @target_container = [*target] # the * is for the generic eager loading code
57
+ @target_container.first
58
+ end
59
+
60
+ def loaded?
61
+ !@target_container.nil?
57
62
  end
58
63
 
59
64
  def after_validation_callback
60
- if @target && !@target.persisted?
65
+ if loaded? && @target_container.first && !@target_container.first.persisted?
61
66
  raise NoBrainer::Error::AssociationNotSaved.new("#{target_name} must be saved first")
62
67
  end
63
68
  end
@@ -16,8 +16,8 @@ module NoBrainer::Document::Association::Core
16
16
  @association_klass ||= self.class.name.deconstantize.constantize
17
17
  end
18
18
 
19
- def new(instance)
20
- association_klass.new(self, instance)
19
+ def new(owner)
20
+ association_klass.new(self, owner)
21
21
  end
22
22
 
23
23
  def delegate(method_name, target, options={})
@@ -25,12 +25,13 @@ module NoBrainer::Document::Association::Core
25
25
  owner_klass.inject_in_layer :associations do
26
26
  define_method(method_name) do |*args, &block|
27
27
  super(*args, &block) if options[:call_super]
28
- association(metadata).__send__(target, *args, &block)
28
+ associations[metadata].__send__(target, *args, &block)
29
29
  end
30
30
  end
31
31
  end
32
32
 
33
33
  def hook
34
+ options.assert_valid_keys(*self.class.const_get(:VALID_OPTIONS))
34
35
  delegate("#{target_name}=", :write)
35
36
  delegate("#{target_name}", :read)
36
37
  end
@@ -39,20 +40,19 @@ module NoBrainer::Document::Association::Core
39
40
  instance_eval <<-RUBY, __FILE__, __LINE__+1
40
41
  if !@added_#{what}
41
42
  metadata = self
42
- owner_klass.#{what} { association(metadata).#{what}_callback }
43
+ owner_klass.#{what} { associations[metadata].#{what}_callback }
43
44
  @added_#{what} = true
44
45
  end
45
46
  RUBY
46
47
  end
47
48
  end
48
49
 
49
- included { attr_accessor :metadata, :instance }
50
+ included { attr_accessor :metadata, :owner }
50
51
 
51
52
  delegate :foreign_key, :target_name, :target_klass, :to => :metadata
52
53
 
53
- def initialize(metadata, instance)
54
- @metadata = metadata
55
- @instance = instance
54
+ def initialize(metadata, owner)
55
+ @metadata, @owner = metadata, owner
56
56
  end
57
57
 
58
58
  def assert_target_type(value)
@@ -0,0 +1,57 @@
1
+ class NoBrainer::Document::Association::EagerLoader
2
+ module Generic
3
+ # Used in associations to declare generic eager loading capabilities
4
+ # The association should implement loaded? and preload.
5
+ def eager_load_with(options={})
6
+ define_method(:eager_load) do |docs, additional_criteria=nil|
7
+ owner_key = instance_exec(&options[:owner_key])
8
+ target_key = instance_exec(&options[:target_key])
9
+
10
+ criteria = target_klass.all
11
+ criteria = criteria.merge(additional_criteria) if additional_criteria
12
+ criteria = criteria.unscoped if options[:unscoped]
13
+
14
+ unloaded_docs = docs.reject { |doc| doc.associations[self].loaded? }
15
+
16
+ owner_keys = unloaded_docs.map(&owner_key).compact.uniq
17
+ if owner_keys.present?
18
+ targets = criteria.where(target_key.in => owner_keys)
19
+ .map { |target| [target.read_attribute(target_key), target] }
20
+ .reduce(Hash.new { |k,v| k[v] = [] }) { |h,(k,v)| h[k] << v; h }
21
+
22
+ unloaded_docs.each do |doc|
23
+ doc_targets = targets[doc.read_attribute(owner_key)]
24
+ doc.associations[self].preload(doc_targets)
25
+ end
26
+ end
27
+
28
+ docs.map { |doc| doc.associations[self].read }.flatten.compact.uniq
29
+ end
30
+ end
31
+ end
32
+
33
+ def eager_load_association(docs, association_name, criteria=nil)
34
+ docs = docs.compact
35
+ return [] if docs.empty?
36
+ association = docs.first.root_class.association_metadata[association_name.to_sym]
37
+ raise "Unknown association #{association_name}" unless association
38
+ association.eager_load(docs, criteria)
39
+ end
40
+
41
+ def eager_load(docs, includes)
42
+ case includes
43
+ when Hash then includes.each do |k,v|
44
+ if v.is_a?(NoBrainer::Criteria)
45
+ v = v.dup
46
+ nested_includes, v._includes = v._includes, []
47
+ eager_load(eager_load_association(docs, k, v), nested_includes)
48
+ else
49
+ eager_load(eager_load_association(docs, k), v)
50
+ end
51
+ end
52
+ when Array then includes.each { |v| eager_load(docs, v) }
53
+ else eager_load_association(docs, includes)
54
+ end
55
+ true
56
+ end
57
+ end
@@ -2,12 +2,13 @@ class NoBrainer::Document::Association::HasMany
2
2
  include NoBrainer::Document::Association::Core
3
3
 
4
4
  class Metadata
5
- VALID_HAS_MANY_OPTIONS = [:foreign_key, :class_name, :dependent]
5
+ VALID_OPTIONS = [:foreign_key, :class_name, :dependent]
6
6
  include NoBrainer::Document::Association::Core::Metadata
7
+ extend NoBrainer::Document::Association::EagerLoader::Generic
7
8
 
8
9
  def foreign_key
9
10
  # TODO test :foreign_key
10
- options[:foreign_key].try(:to_sym) || :"#{owner_klass.name.underscore}_id"
11
+ options[:foreign_key].try(:to_sym) || owner_klass.name.foreign_key.to_sym
11
12
  end
12
13
 
13
14
  def target_klass
@@ -15,47 +16,69 @@ class NoBrainer::Document::Association::HasMany
15
16
  (options[:class_name] || target_name.to_s.singularize.camelize).constantize
16
17
  end
17
18
 
19
+ def inverses
20
+ # We can always infer the inverse association of a has_many relationship,
21
+ # because a belongs_to association cannot have a scope applied on the
22
+ # selector.
23
+ # XXX Without caching, this is going to get CPU intensive quickly, but
24
+ # caching is hard (rails console reload, etc.).
25
+ target_klass.association_metadata.values.select do |assoc|
26
+ assoc.is_a?(NoBrainer::Document::Association::BelongsTo::Metadata) and
27
+ assoc.foreign_key == self.foreign_key and
28
+ assoc.target_klass.root_class == owner_klass.root_class
29
+ end
30
+ end
31
+
18
32
  def hook
19
33
  super
20
- options.assert_valid_keys(*VALID_HAS_MANY_OPTIONS)
21
- add_callback_for(:before_destroy)
34
+ add_callback_for(:before_destroy) if options[:dependent]
22
35
  end
23
36
 
24
- def eager_load(docs, criteria=nil)
25
- target_criteria = target_klass.all
26
- target_criteria = target_criteria.merge(criteria) if criteria
27
- docs_ids = Hash[docs.map { |doc| [doc, doc.id] }]
28
- fk_targets = target_criteria
29
- .where(foreign_key.in => docs_ids.values)
30
- .reduce({}) do |hash, doc|
31
- fk = doc.read_attribute(foreign_key)
32
- hash[fk] ||= []
33
- hash[fk] << doc
34
- hash
35
- end
36
- docs_ids.each { |doc, id| doc.association(self)._write(fk_targets[id]) if fk_targets[id] }
37
- fk_targets.values.flatten(1)
38
- end
37
+ eager_load_with :owner_key => ->{ :id }, :target_key => ->{ foreign_key }
39
38
  end
40
39
 
41
- def children_criteria
42
- @children_criteria ||= target_klass.where(foreign_key => instance.id)
40
+ def target_criteria
41
+ @target_criteria ||= target_klass.where(foreign_key => owner.id)
42
+ ._after_instantiate(set_inverse_proc)
43
43
  end
44
44
 
45
45
  def read
46
- children_criteria
46
+ target_criteria
47
47
  end
48
48
 
49
49
  def write(new_children)
50
- raise "You can't assign the array of #{target_name}. Instead, you must modify delete and create #{target_klass} manually."
50
+ raise "You can't assign #{target_name}. " +
51
+ "Instead, you must modify delete and create #{target_klass} manually."
52
+ end
53
+
54
+ def loaded?
55
+ target_criteria.cached?
56
+ end
57
+
58
+ def preload(new_targets)
59
+ set_inverses_of(new_targets)
60
+ target_criteria._override_cache(new_targets)
61
+ end
62
+
63
+ def set_inverses_of(new_targets)
64
+ @inverses ||= metadata.inverses
65
+ return if @inverses.blank?
66
+
67
+ new_targets.each do |target|
68
+ # We don't care if target is a parent class where the inverse association
69
+ # is defined, we set the association regardless.
70
+ # The user won't be able to access it since the association accessors are
71
+ # not defined on the parent class.
72
+ @inverses.each { |inverse| target.associations[inverse].preload(self.owner) }
73
+ end
51
74
  end
52
75
 
53
- def _write(new_children)
54
- children_criteria._override_cache(new_children)
76
+ def set_inverse_proc
77
+ lambda { |target| set_inverses_of([target]) if target.is_a?(NoBrainer::Document) }
55
78
  end
56
79
 
57
80
  def before_destroy_callback
58
- criteria = children_criteria.unscoped
81
+ criteria = target_criteria.unscoped.without_cache
59
82
  case metadata.options[:dependent]
60
83
  when nil then
61
84
  when :destroy then criteria.destroy_all