nobrainer 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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