Commit 43c800477702e27340372f56c1e529cf9ebd0f3d

Merge commit 'sebastian/master'

Commit diff

README.txt

 
635635 "before_delete",
636636 "after_delete"]
637637
638
638=== Smart save
639
640The way a CouchObject instance acts by default is to save itself and all
641its relatives regardless of if it needs to be saved or not when the +save+
642method is called.
643
644When activating smart save each instance contains a copy of its original
645state, and can, based on that, make smart decisions regarding wether or
646not it actually needs to save.
647
648Smart save should be used selectively though because keeping a copy of it's
649original state will increase the memory usage drastically for objects that
650contain a lot of data! There is also an overhead while initializing the
651object when the state is created, although that will in most cases be
652rather insignificant. These factors add up quickly though if you are working
653with many objects at the same time.
654
655Smart save can either be defined in the class definition:
656
657 class SmartClass
658 include CouchObject::Persistable
659 smart_save
660 end
661
662 instance_with_smart_save_activated = SmartClass.get("foo")
663
664where all instances will receive the smart saving feature by default,
665or it can be toggled manually:
666
667 class NotSoSmartClass
668 include CouchObject::Persistable
669 end
670
671 instance_WITHOUT_smart_save_activated = NotSoSmartClass.get("foo")
672
673 NotSoSmartClass.smart_save
674
675 instance_WITH_smart_save_activated = NotSoSmartClass.get("bar")
676
677 NotSoSmartClass.deactivate_smart_save
678
679 instance_WITHOUT_smart_save_activated_2 = NotSoSmartClass.get("dong")
680
681The same as in the example above can be achieved with the convenience
682method +get_with_smart_save+ which takes the same parameters as +get_by_id+.
683The same example could therefor also be written as:
684
685 class NotSoSmartClass
686 include CouchObject::Persistable
687 end
688
689 instance_WITHOUT_smart_save_activated = NotSoSmartClass.get("foo")
690
691 instance_WITH_smart_save_activated = NotSoSmartClass.get_with_smart_save("bar")
692
693 instance_WITHOUT_smart_save_activated_2 = NotSoSmartClass.get("dong")
694
toggle raw diff

lib/couch_object.rb

 
1111end
1212
1313require 'json/add/core'
14require "ruby2ruby"
14require 'thread'
15
16# require "ruby2ruby" Is used by the ruby view server which is to be moved out of the gem
17# require "htmlentities" # if you want the UTILS::decode_strings to
18 # replace characters like ø with ø
19 # then this has to be included...
20require "kconv" # To convert all input to UTF-8 before it is made to_json
1521
1622$:.unshift File.dirname(__FILE__)
1723
toggle raw diff

lib/couch_object/persistable.rb

 
1111#
1212# = TODOs and known issues:
1313#
14# * TODO: Create a way to check if an object has unsaved changes or not and
15# make it check that itself before saving
14# * TODO: create a way to solve saving conflicts. Should also be possible
15# to chose if only conflicts for certain variables should be handled.
1616#
1717# * TODO: If real world usage shows that it is needed then create a reload method
1818# that reloads the content from the db. Might be useful if some other
104104 end
105105
106106 # creates a new object and initialize all its sub objects
107 new_object = coach_load_object(response)
107 new_object = couch_load_object(response)
108108
109109 # set the storage location it was loaded from so it can be saved
110110 # back directly without having to supply the db_uri again
119119 #
120120 alias get get_by_id
121121
122 #
123 # Takes, returns and raises the same things as +get_by_id+
124 #
125 # Creates a new object that is forced into smart save mode
126 # although the class it is stemming from might not have smart
127 # saving enabled.
128 #
129 def get_with_smart_save(id, db_uri = self.location)
130
131 new_object = self.get_by_id(id, db_uri)
132
133 # Force it into smart save mode
134 new_object.couch_force_smart_save
135
136 # Initialize the original state.
137 new_object.couch_set_initial_state
138
139 new_object
140 end
122141
123142 #
124143 # Loads all document from a given view from the database
201201 if response["error"]
202202
203203 response["rows"].each do |params_for_object|
204 objects_to_return << coach_load_object(params_for_object["value"])
204 objects_to_return << couch_load_object(params_for_object["value"])
205205 end
206206
207207 objects_to_return
222222 # Raises:
223223 # * Does currently not raise any error
224224 #
225 def coach_load_object(parameters)
225 def couch_load_object(parameters)
226226
227227 # Getting the values that shouldn't be passed on to the initializer
228228 id = parameters["_id"]
251251 parameters["attributes"][key]["class"] != nil
252252
253253 new_object_from_couch.instance_variable_set("@#{key}", \
254 self.coach_load_object(parameters["attributes"][key]))
254 self.couch_load_object(parameters["attributes"][key]))
255255 else
256256 # Add the value to the class
257257 new_object_from_couch.
280280 instance_variable_set("@belongs_to", belongs_to) \
281281 if belongs_to
282282
283 new_object_from_couch.couch_set_initial_state
283284
284285 # Returns the new object
285286 new_object_from_couch
308308 id.nil? || revision.nil?
309309 end
310310
311 #
312 # Stores the initial value of the instance to a variable
313 # for later reference by the +unsaved_changes?+ method
314 #
315 def couch_set_initial_state
316 # For the unsaved_changes? instance method to work, we have to
317 # supply a snapshot of what the fresh object looked like.
318 # BUT ONLY if the user has activated the smart_save option
319 if use_smart_save
320
321 @couch_initial_load = true
322 @couch_object_original_state = to_json
323 @couch_initial_load = false
324
325 end
326 end
327
328 #
329 # Forces the instance object into smart save mode
330 #
331 def couch_force_smart_save
332 def self.use_smart_save
333 true
334 end
335 end
336
311337 #
312338 # Saves the object to the db_uri supplied, or if not set, to the
313339 # location the object has previously been saved to.
364364 # If it's belongs_to relationships haven't been saved
365365 # then it has be done first.
366366 # Saving the master will also automatically save the child
367
368367 performed_save = false
368
369 # If the belongs_to relationships haven't already been loaded,
370 # there is reason to believe that:
371 # * The object is new and doesn't have any relation set
372 # * The object already knows about it's belongs_to relations
373 # and doesn't need aditional information about them for saving
374 # We therefore deactivate the loading of belongs_to relations
375 original_state_of_load_belongs_to_relations = \
376 @do_not_load_belongs_to_relations
377 @do_not_load_belongs_to_relations = true
378
369379 belongs_to.each do |what_it_belongs_to|
370380 master_class = self.send(what_it_belongs_to)
371381 unless master_class == nil || !master_class.new?
384384 end
385385 end
386386
387 # Reset the do_not_load_belongs_to_relations variable to its
388 # original state
389 @do_not_load_belongs_to_relations = \
390 original_state_of_load_belongs_to_relations
391
387392 # If none of the master classes were saved, meaning they weren't new
388393 # or didn't exist, then we have to manually save this object.
389394 couch_perform_save unless performed_save
410410 state_before_wants_to_load_relations = @do_not_load_has_many_relations
411411 @do_not_load_has_many_relations = true
412412
413 # We thread the save process in case the relations do
414 # some funky time consuming stuff in their call backs
415 threads = []
413416 has_many.each do |thing_it_has_many_of|
414417 self.send(thing_it_has_many_of).each do |related_object|
415 related_object.save(location)
418 threads << Thread.new(related_object) do |object_to_save|
419 object_to_save.save(location)
420 end
416421 end
417422 end
423 threads.each {|thr| thr.join}
418424
419425 # Save all the has_one relations
420426 has_one.each do |thing_it_has_one_of|
434434 # Reset the do_not_load_has_many_relations variable
435435 # to it's original state
436436 @do_not_load_has_many_relations = state_before_wants_to_load_relations
437
437
438438 perform_callback(:after_save)
439439
440440 return the_return_value
454454
455455 db = CouchObject::Database.open(location)
456456
457 unless (response = db.post("", self.to_json)).to_document["error"]
457 json_value = self.to_json
458
459 unless (response = db.post("", json_value)).to_document["error"]
458460 response_document = response.to_document
459461 @id = response_document.id
460462 @revision = response_document.revision
465465 @updated_at = Time.now if self.
466466 class::couch_object_timestamp_on_update?
467467
468 # If the user has activated smart_save, we should set the state
469 # to the new contents of this instance!
470 @couch_object_original_state = json_value if use_smart_save
471
468472 perform_callback(:after_create)
469473
470474 # Returns a hash with the ID and Revision
476476
477477 else
478478
479 raise CouchObject::Errors::DatabaseSaveFailed, "The document couldn't be created.\n" + \
479 raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
480 "couldn't be created.\n" + \
480481 "CouchDB reported: #{response.to_document["error"]}"
481482
482483 end
494494 #
495495 def couch_update
496496
497 perform_callback(:before_update)
498
499 # Please notice the following:
500 # The response from CouchDB only includes the revision number
501 # and the ID so the updated_at value in the document store
502 # differs from the updated_at value in the class
503 @updated_at = Time.now if self.
504 class::couch_object_timestamp_on_update?
497 # Only save if it has unsaved changes!
498 if unsaved_changes?
505499
506 db = CouchObject::Database.open(location)
507
508 unless (response = db.put(id, self.to_json)).to_document["error"]
500 perform_callback(:before_update)
509501
510 @revision = response.to_document.revision
502 # Please notice the following:
503 # The response from CouchDB only includes the revision number
504 # and the ID so the updated_at value in the document store
505 # differs from the updated_at value in the class
506 @updated_at = Time.now if self.
507 class::couch_object_timestamp_on_update?
511508
512 perform_callback(:after_update)
509 db = CouchObject::Database.open(location)
513510
514 # Returns a hash with the ID and Revision
515 {:id => @id, :revision => @revision}
516
517 else
511 json_value = self.to_json
512
513 unless (response = db.put(id, json_value)).to_document["error"]
514
515 @revision = response.to_document.revision
516
517 # If the user has activated smart_save, we should set the state
518 # to the new contents of this instance!
519 @couch_object_original_state = json_value if use_smart_save
520
521 perform_callback(:after_update)
522
523 # Returns a hash with the ID and Revision
524 {:id => @id, :revision => @revision}
518525
519 raise CouchObject::Errors::DatabaseSaveFailed, "The document couldn't be updated.\n" + \
520 "The reason might be a revision number conflict.\n" + \
521 "CouchDB reported: #{response.to_document["reason"]}"
526 else
527
528 raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
529 "couldn't be updated.\n" + \
530 "The reason might be a revision number conflict.\n" + \
531 "CouchDB reported: #{response.to_document["reason"]}"
532
533 end
522534
535
523536 end
537
524538 end
525539
526
540
527541 public
528542 #
543 # Classes WITH smart_save activated:
544 # Any instance should be able to know if it has unsaved changes or not.
545 # When an instance is loaded from the DB it creates a snapshot of what
546 # its variables contain. Based on a comparison between the snapshot
547 # and the contents of the instance this method returns true or false.
548 #
549 # Classes WITHOUT smart_save activated:
550 # Will always return true regardless of what state it is in
551 #
552 # A new object will always return true
553 #
554 # Returns:
555 # * true: if it has changes that haven't been saved to the database
556 # * fase: if the nothing has changed since it was loaded from the
557 # database.
558 #
559 def unsaved_changes?
560 return true if new?
561 return true unless use_smart_save
562
563 @couch_object_original_state == self.to_json ? false : true
564
565 end
566
567 #
529568 # Any instance should be able to delete itself
530569 #
531570 # Takes:
685685 "@updated_at",
686686 "@id",
687687 "@revision",
688 "@do_not_load_has_many_relations"]
688 "@do_not_load_has_many_relations",
689 "@do_not_load_belongs_to_relations",
690 "@couch_object_original_state",
691 "@couch_initial_load",
692 "@belongs_to"]
689693
690694 # We also have to remove all the objects that are related
691695 # through belongs_to and has_many relations. They have to be called
725725
726726 # If it is in belongs_to relationship(s), then that fact
727727 # has to be stored in the database
728 unless belongs_to == []
728 if belongs_to != [] and !@couch_initial_load # the couch_initial_load
729 # is to get the initial
730 # state of the object
731 # without loading the
732 # belongs_to relations
733 # which would start an
734 # infinite loop.
735 # NOTE: this is only for
736 # cases where smart_save
737 # has been activated.
738
739 # No reason to load the belongs_to relations if they haven't
740 # already been loaded from the database!
741 original_state_of_load_belongs_to_relations = \
742 @do_not_load_belongs_to_relations
743 @do_not_load_belongs_to_relations = true
744
729745 what_it_belongs_to = {}
730746 self.send(:belongs_to).each do |relation|
731747 as_what = self.send("belongs_to_#{relation}_as")
732748 object_it_belongs_to = self.send(relation)
733
749
734750 # Unless the relation is unset, in which case it will be of
735751 # type NilClass, set it.
736 what_it_belongs_to[as_what] = object_it_belongs_to.id \
752 what_it_belongs_to[as_what.to_s] = object_it_belongs_to.id || "new" \
737753 unless object_it_belongs_to.class == NilClass
738754 end
739 parameters["belongs_to"] = what_it_belongs_to
755
756 # Reset the value so they are loaded the next time when needed
757 # if that is what the user wants.
758 @do_not_load_belongs_to_relations = \
759 original_state_of_load_belongs_to_relations
760
761
762 # We have to make sure the @belongs_to variable contains all changes
763 # and all the original values for the keys that haven't changed/
764 # relations that haven't been loaded
765 original_belongs_to = @belongs_to || {}
766 @belongs_to = what_it_belongs_to
767 times_through = 1
768
769 # LOOP 1... see below for problem description
770 original_belongs_to.each_pair do |key, value|
771 times_through += 1
772 @belongs_to[key.to_s] = value unless @belongs_to[key.to_s]
773 end
774
775 # FIXME:
776 # Now... this is a really hacky way to solve this problem
777 # and should be improved... Feel free to come up with sollutions
778 # Case:
779 # If it has a belongs_to relationship that is new and therefore
780 # doesn't have an ID it would normally be written in the @belongs_to
781 # variable as nil. The problem is that if self has previously
782 # been saved with another parent object this ID would still come
783 # through in the @belongs_to variable updater (see "LOOP 1" above).
784 # We therefore assign the ID "new" to all unsaved relations, which we
785 # now have to nilify. If we don't the smart save wont work for this
786 # type of cases.
787
788 end
789
790 parameters["belongs_to"] = @belongs_to
791 parameters.delete("belongs_to") if @belongs_to == {} or @belongs_to == nil
792
793 # if @couch_initial_load && @belongs_to
794
795 begin
796 parameters.to_json
797 rescue JSON::GeneratorError
798 # All strings aren't encoded properly, so we have to force them into
799 # UTF-8.
800 # FIXME: The kconv library has some weird artefacts though where
801 # a lot of Norwegian (Scandianavian?) letters get turned into
802 # asian characters of some sort!
803 CouchObject::Utils::decode_strings(parameters).to_json
740804 end
741805
742 parameters.to_json
743806 end
744807
808
809
745810 #
746811 # Sometimes you might want to add an object to
747812 # a has_many relation without interacting with the other relations at all.
820820 def do_load_has_many_relations
821821 @do_not_load_has_many_relations = false
822822 end
823 alias do_not_load_has_one_relations do_not_load_has_many_relations
824 alias do_load_has_one_relations do_load_has_many_relations
825 alias do_not_load_has_one_relation do_not_load_has_many_relations
826 alias do_load_has_one_relation do_load_has_many_relations
827
828 #
829 # If you need to access the belongs_to variable without loading the
830 # relation if it hasn't already been loaded, you can call the instance
831 # method +do_not_load_belongs_to_relations+. To reactivate loading so
832 # the relation is loaded the next time it is needed, call the instance
833 # method +do_load_belongs_to_relations+.
834 #
835 def do_not_load_belongs_to_relations
836 @do_not_load_belongs_to_relations = true
837 end
838 def do_load_belongs_to_relations
839 @do_not_load_belongs_to_relations = false
840 end
841 alias do_not_load_has_many_relation do_not_load_has_many_relations
842 alias do_load_belongs_to_relation do_load_belongs_to_relations
823843
824844
825845 protected
899899
900900 db = CouchObject::Database.open(location)
901901
902 if (response = db.put("_design/#{view_name}/#{view_name}", \
902 if (response = db.put("_design%2F#{view_name}", \
903903 view_code_query))
904904 results = couch_load_has_many_relations(which_relation)
905905 else
916916 #
917917 def couch_load_belongs_to_relation(that_is_called)
918918
919 return nil if new? || @do_not_load_belongs_to_relations
920
919921 # If it doesn't have a belongs to ID then there is no
920922 # related object in the database
921923 if @belongs_to && @belongs_to[that_is_called]
toggle raw diff

lib/couch_object/persistable/meta_classes.rb

 
115115
116116
117117 ##
118 # Smart savign
119 ##
120
121 def use_smart_save; false; end
122 #
123 # Smart save (defaults to false), if activated, keeps a snapshot of
124 # the objects initial state and evaluates if the class needs to be
125 # saved to the database by comparing it to the snapshot when a save
126 # is requested.
127 #
128 # Please notice:
129 # Only activate this feature in cases where it is needed.
130 # It might slow down the performance of your app if you activate it
131 # for classes that you need many instances of and that you won't
132 # call the save method on after having loaded them from the database.
133 # Please also bare in mind that the class instance will store an
134 # extra copy of its contents which will lead to quite a big memory
135 # overhead for classes that store a lot of data!
136 #
137 def self.smart_save
138 self.instance_eval do
139 define_method("use_smart_save") do
140 true
141 end
142 end
143 end
144 #
145 # Smart save can also be used on a per-case basis if it is sometimes
146 # needed and sometimes not.
147 #
148 # Example:
149 #
150 # user_without_smart_save_1 = User.get("foo")
151 # User.smart_save
152 # user_with_smart_save = User.get("bar")
153 # User.deactivate_smart_save
154 # user_without_smart_save_2 = User.get("bong")
155 #
156 def self.deactivate_smart_save
157 self.instance_eval do
158 define_method("use_smart_save") do
159 false
160 end
161 end
162 end
163
164
165
166 ##
118167 # Relations
119168 ##
120169
301301 else
302302 is_a_has_many_relationship = true
303303 end
304
304
305 # Now... there is no good reason loading a belongs_to relation
306 # from the database only to remove the relation with the child,
307 # because the relation is stored in the child anyway...
308 # We therefore temporarily deactivate the loading of belongs_to
309 # relations
310 original_state_of_load_belongs_to_relations = \
311 @do_not_load_belongs_to_relations
312 @do_not_load_belongs_to_relations = true
305313
306314 if is_a_has_many_relationship
307315
339339 if object_to_add
340340
341341 end
342
343 # And now we reset the @do_not_load_belongs_to_relations
344 # variable to its original value:
345 @do_not_load_belongs_to_relations = \
346 original_state_of_load_belongs_to_relations
347
342348 end
343349
344350 # Setter without callback for new objects
toggle raw diff