| |   |
| 11 | 11 | # |
| 12 | 12 | # = TODOs and known issues: |
| 13 | 13 | # |
| 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. |
| 16 | 16 | # |
| 17 | 17 | # * TODO: If real world usage shows that it is needed then create a reload method |
| 18 | 18 | # that reloads the content from the db. Might be useful if some other |
| … | … | |
| 104 | 104 | end |
| 105 | 105 | |
| 106 | 106 | # creates a new object and initialize all its sub objects |
| 107 | | new_object = coach_load_object(response) |
| 107 | new_object = couch_load_object(response) |
| 108 | 108 | |
| 109 | 109 | # set the storage location it was loaded from so it can be saved |
| 110 | 110 | # back directly without having to supply the db_uri again |
| … | … | |
| 119 | 119 | # |
| 120 | 120 | alias get get_by_id |
| 121 | 121 | |
| 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 |
| 122 | 141 | |
| 123 | 142 | # |
| 124 | 143 | # Loads all document from a given view from the database |
| … | … | |
| 201 | 201 | if response["error"] |
| 202 | 202 | |
| 203 | 203 | 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"]) |
| 205 | 205 | end |
| 206 | 206 | |
| 207 | 207 | objects_to_return |
| … | … | |
| 222 | 222 | # Raises: |
| 223 | 223 | # * Does currently not raise any error |
| 224 | 224 | # |
| 225 | | def coach_load_object(parameters) |
| 225 | def couch_load_object(parameters) |
| 226 | 226 | |
| 227 | 227 | # Getting the values that shouldn't be passed on to the initializer |
| 228 | 228 | id = parameters["_id"] |
| … | … | |
| 251 | 251 | parameters["attributes"][key]["class"] != nil |
| 252 | 252 | |
| 253 | 253 | new_object_from_couch.instance_variable_set("@#{key}", \ |
| 254 | | self.coach_load_object(parameters["attributes"][key])) |
| 254 | self.couch_load_object(parameters["attributes"][key])) |
| 255 | 255 | else |
| 256 | 256 | # Add the value to the class |
| 257 | 257 | new_object_from_couch. |
| … | … | |
| 280 | 280 | instance_variable_set("@belongs_to", belongs_to) \ |
| 281 | 281 | if belongs_to |
| 282 | 282 | |
| 283 | new_object_from_couch.couch_set_initial_state |
| 283 | 284 | |
| 284 | 285 | # Returns the new object |
| 285 | 286 | new_object_from_couch |
| … | … | |
| 308 | 308 | id.nil? || revision.nil? |
| 309 | 309 | end |
| 310 | 310 | |
| 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 | |
| 311 | 337 | # |
| 312 | 338 | # Saves the object to the db_uri supplied, or if not set, to the |
| 313 | 339 | # location the object has previously been saved to. |
| … | … | |
| 364 | 364 | # If it's belongs_to relationships haven't been saved |
| 365 | 365 | # then it has be done first. |
| 366 | 366 | # Saving the master will also automatically save the child |
| 367 | | |
| 368 | 367 | 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 | |
| 369 | 379 | belongs_to.each do |what_it_belongs_to| |
| 370 | 380 | master_class = self.send(what_it_belongs_to) |
| 371 | 381 | unless master_class == nil || !master_class.new? |
| … | … | |
| 384 | 384 | end |
| 385 | 385 | end |
| 386 | 386 | |
| 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 | |
| 387 | 392 | # If none of the master classes were saved, meaning they weren't new |
| 388 | 393 | # or didn't exist, then we have to manually save this object. |
| 389 | 394 | couch_perform_save unless performed_save |
| … | … | |
| 410 | 410 | state_before_wants_to_load_relations = @do_not_load_has_many_relations |
| 411 | 411 | @do_not_load_has_many_relations = true |
| 412 | 412 | |
| 413 | # We thread the save process in case the relations do |
| 414 | # some funky time consuming stuff in their call backs |
| 415 | threads = [] |
| 413 | 416 | has_many.each do |thing_it_has_many_of| |
| 414 | 417 | 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 |
| 416 | 421 | end |
| 417 | 422 | end |
| 423 | threads.each {|thr| thr.join} |
| 418 | 424 | |
| 419 | 425 | # Save all the has_one relations |
| 420 | 426 | has_one.each do |thing_it_has_one_of| |
| … | … | |
| 434 | 434 | # Reset the do_not_load_has_many_relations variable |
| 435 | 435 | # to it's original state |
| 436 | 436 | @do_not_load_has_many_relations = state_before_wants_to_load_relations |
| 437 | | |
| 437 | |
| 438 | 438 | perform_callback(:after_save) |
| 439 | 439 | |
| 440 | 440 | return the_return_value |
| … | … | |
| 454 | 454 | |
| 455 | 455 | db = CouchObject::Database.open(location) |
| 456 | 456 | |
| 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"] |
| 458 | 460 | response_document = response.to_document |
| 459 | 461 | @id = response_document.id |
| 460 | 462 | @revision = response_document.revision |
| … | … | |
| 465 | 465 | @updated_at = Time.now if self. |
| 466 | 466 | class::couch_object_timestamp_on_update? |
| 467 | 467 | |
| 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 | |
| 468 | 472 | perform_callback(:after_create) |
| 469 | 473 | |
| 470 | 474 | # Returns a hash with the ID and Revision |
| … | … | |
| 476 | 476 | |
| 477 | 477 | else |
| 478 | 478 | |
| 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" + \ |
| 480 | 481 | "CouchDB reported: #{response.to_document["error"]}" |
| 481 | 482 | |
| 482 | 483 | end |
| … | … | |
| 494 | 494 | # |
| 495 | 495 | def couch_update |
| 496 | 496 | |
| 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? |
| 505 | 499 | |
| 506 | | db = CouchObject::Database.open(location) |
| 507 | | |
| 508 | | unless (response = db.put(id, self.to_json)).to_document["error"] |
| 500 | perform_callback(:before_update) |
| 509 | 501 | |
| 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? |
| 511 | 508 | |
| 512 | | perform_callback(:after_update) |
| 509 | db = CouchObject::Database.open(location) |
| 513 | 510 | |
| 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} |
| 518 | 525 | |
| 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 |
| 522 | 534 | |
| 535 | |
| 523 | 536 | end |
| 537 | |
| 524 | 538 | end |
| 525 | 539 | |
| 526 | | |
| 540 | |
| 527 | 541 | public |
| 528 | 542 | # |
| 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 | # |
| 529 | 568 | # Any instance should be able to delete itself |
| 530 | 569 | # |
| 531 | 570 | # Takes: |
| … | … | |
| 685 | 685 | "@updated_at", |
| 686 | 686 | "@id", |
| 687 | 687 | "@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"] |
| 689 | 693 | |
| 690 | 694 | # We also have to remove all the objects that are related |
| 691 | 695 | # through belongs_to and has_many relations. They have to be called |
| … | … | |
| 725 | 725 | |
| 726 | 726 | # If it is in belongs_to relationship(s), then that fact |
| 727 | 727 | # 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 | |
| 729 | 745 | what_it_belongs_to = {} |
| 730 | 746 | self.send(:belongs_to).each do |relation| |
| 731 | 747 | as_what = self.send("belongs_to_#{relation}_as") |
| 732 | 748 | object_it_belongs_to = self.send(relation) |
| 733 | | |
| 749 | |
| 734 | 750 | # Unless the relation is unset, in which case it will be of |
| 735 | 751 | # 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" \ |
| 737 | 753 | unless object_it_belongs_to.class == NilClass |
| 738 | 754 | 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 |
| 740 | 804 | end |
| 741 | 805 | |
| 742 | | parameters.to_json |
| 743 | 806 | end |
| 744 | 807 | |
| 808 | |
| 809 | |
| 745 | 810 | # |
| 746 | 811 | # Sometimes you might want to add an object to |
| 747 | 812 | # a has_many relation without interacting with the other relations at all. |
| … | … | |
| 820 | 820 | def do_load_has_many_relations |
| 821 | 821 | @do_not_load_has_many_relations = false |
| 822 | 822 | 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 |
| 823 | 843 | |
| 824 | 844 | |
| 825 | 845 | protected |
| … | … | |
| 899 | 899 | |
| 900 | 900 | db = CouchObject::Database.open(location) |
| 901 | 901 | |
| 902 | | if (response = db.put("_design/#{view_name}/#{view_name}", \ |
| 902 | if (response = db.put("_design%2F#{view_name}", \ |
| 903 | 903 | view_code_query)) |
| 904 | 904 | results = couch_load_has_many_relations(which_relation) |
| 905 | 905 | else |
| … | … | |
| 916 | 916 | # |
| 917 | 917 | def couch_load_belongs_to_relation(that_is_called) |
| 918 | 918 | |
| 919 | return nil if new? || @do_not_load_belongs_to_relations |
| 920 | |
| 919 | 921 | # If it doesn't have a belongs to ID then there is no |
| 920 | 922 | # related object in the database |
| 921 | 923 | if @belongs_to && @belongs_to[that_is_called] |
| toggle raw diff |
--- a/lib/couch_object/persistable.rb
+++ b/lib/couch_object/persistable.rb
@@ -11,8 +11,8 @@
#
# = TODOs and known issues:
#
-# * TODO: Create a way to check if an object has unsaved changes or not and
-# make it check that itself before saving
+# * TODO: create a way to solve saving conflicts. Should also be possible
+# to chose if only conflicts for certain variables should be handled.
#
# * TODO: If real world usage shows that it is needed then create a reload method
# that reloads the content from the db. Might be useful if some other
@@ -104,7 +104,7 @@ module CouchObject
end
# creates a new object and initialize all its sub objects
- new_object = coach_load_object(response)
+ new_object = couch_load_object(response)
# set the storage location it was loaded from so it can be saved
# back directly without having to supply the db_uri again
@@ -119,6 +119,25 @@ module CouchObject
#
alias get get_by_id
+ #
+ # Takes, returns and raises the same things as +get_by_id+
+ #
+ # Creates a new object that is forced into smart save mode
+ # although the class it is stemming from might not have smart
+ # saving enabled.
+ #
+ def get_with_smart_save(id, db_uri = self.location)
+
+ new_object = self.get_by_id(id, db_uri)
+
+ # Force it into smart save mode
+ new_object.couch_force_smart_save
+
+ # Initialize the original state.
+ new_object.couch_set_initial_state
+
+ new_object
+ end
#
# Loads all document from a given view from the database
@@ -182,7 +201,7 @@ module CouchObject
if response["error"]
response["rows"].each do |params_for_object|
- objects_to_return << coach_load_object(params_for_object["value"])
+ objects_to_return << couch_load_object(params_for_object["value"])
end
objects_to_return
@@ -203,7 +222,7 @@ module CouchObject
# Raises:
# * Does currently not raise any error
#
- def coach_load_object(parameters)
+ def couch_load_object(parameters)
# Getting the values that shouldn't be passed on to the initializer
id = parameters["_id"]
@@ -232,7 +251,7 @@ module CouchObject
parameters["attributes"][key]["class"] != nil
new_object_from_couch.instance_variable_set("@#{key}", \
- self.coach_load_object(parameters["attributes"][key]))
+ self.couch_load_object(parameters["attributes"][key]))
else
# Add the value to the class
new_object_from_couch.
@@ -261,6 +280,7 @@ module CouchObject
instance_variable_set("@belongs_to", belongs_to) \
if belongs_to
+ new_object_from_couch.couch_set_initial_state
# Returns the new object
new_object_from_couch
@@ -288,6 +308,32 @@ module CouchObject
id.nil? || revision.nil?
end
+ #
+ # Stores the initial value of the instance to a variable
+ # for later reference by the +unsaved_changes?+ method
+ #
+ def couch_set_initial_state
+ # For the unsaved_changes? instance method to work, we have to
+ # supply a snapshot of what the fresh object looked like.
+ # BUT ONLY if the user has activated the smart_save option
+ if use_smart_save
+
+ @couch_initial_load = true
+ @couch_object_original_state = to_json
+ @couch_initial_load = false
+
+ end
+ end
+
+ #
+ # Forces the instance object into smart save mode
+ #
+ def couch_force_smart_save
+ def self.use_smart_save
+ true
+ end
+ end
+
#
# Saves the object to the db_uri supplied, or if not set, to the
# location the object has previously been saved to.
@@ -318,8 +364,18 @@ module CouchObject
# If it's belongs_to relationships haven't been saved
# then it has be done first.
# Saving the master will also automatically save the child
-
performed_save = false
+
+ # If the belongs_to relationships haven't already been loaded,
+ # there is reason to believe that:
+ # * The object is new and doesn't have any relation set
+ # * The object already knows about it's belongs_to relations
+ # and doesn't need aditional information about them for saving
+ # We therefore deactivate the loading of belongs_to relations
+ original_state_of_load_belongs_to_relations = \
+ @do_not_load_belongs_to_relations
+ @do_not_load_belongs_to_relations = true
+
belongs_to.each do |what_it_belongs_to|
master_class = self.send(what_it_belongs_to)
unless master_class == nil || !master_class.new?
@@ -328,6 +384,11 @@ module CouchObject
end
end
+ # Reset the do_not_load_belongs_to_relations variable to its
+ # original state
+ @do_not_load_belongs_to_relations = \
+ original_state_of_load_belongs_to_relations
+
# If none of the master classes were saved, meaning they weren't new
# or didn't exist, then we have to manually save this object.
couch_perform_save unless performed_save
@@ -349,11 +410,17 @@ module CouchObject
state_before_wants_to_load_relations = @do_not_load_has_many_relations
@do_not_load_has_many_relations = true
+ # We thread the save process in case the relations do
+ # some funky time consuming stuff in their call backs
+ threads = []
has_many.each do |thing_it_has_many_of|
self.send(thing_it_has_many_of).each do |related_object|
- related_object.save(location)
+ threads << Thread.new(related_object) do |object_to_save|
+ object_to_save.save(location)
+ end
end
end
+ threads.each {|thr| thr.join}
# Save all the has_one relations
has_one.each do |thing_it_has_one_of|
@@ -367,7 +434,7 @@ module CouchObject
# Reset the do_not_load_has_many_relations variable
# to it's original state
@do_not_load_has_many_relations = state_before_wants_to_load_relations
-
+
perform_callback(:after_save)
return the_return_value
@@ -387,7 +454,9 @@ module CouchObject
db = CouchObject::Database.open(location)
- unless (response = db.post("", self.to_json)).to_document["error"]
+ json_value = self.to_json
+
+ unless (response = db.post("", json_value)).to_document["error"]
response_document = response.to_document
@id = response_document.id
@revision = response_document.revision
@@ -396,6 +465,10 @@ module CouchObject
@updated_at = Time.now if self.
class::couch_object_timestamp_on_update?
+ # If the user has activated smart_save, we should set the state
+ # to the new contents of this instance!
+ @couch_object_original_state = json_value if use_smart_save
+
perform_callback(:after_create)
# Returns a hash with the ID and Revision
@@ -403,7 +476,8 @@ module CouchObject
else
- raise CouchObject::Errors::DatabaseSaveFailed, "The document couldn't be created.\n" + \
+ raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
+ "couldn't be created.\n" + \
"CouchDB reported: #{response.to_document["error"]}"
end
@@ -420,38 +494,77 @@ module CouchObject
#
def couch_update
- perform_callback(:before_update)
-
- # Please notice the following:
- # The response from CouchDB only includes the revision number
- # and the ID so the updated_at value in the document store
- # differs from the updated_at value in the class
- @updated_at = Time.now if self.
- class::couch_object_timestamp_on_update?
+ # Only save if it has unsaved changes!
+ if unsaved_changes?
- db = CouchObject::Database.open(location)
-
- unless (response = db.put(id, self.to_json)).to_document["error"]
+ perform_callback(:before_update)
- @revision = response.to_document.revision
+ # Please notice the following:
+ # The response from CouchDB only includes the revision number
+ # and the ID so the updated_at value in the document store
+ # differs from the updated_at value in the class
+ @updated_at = Time.now if self.
+ class::couch_object_timestamp_on_update?
- perform_callback(:after_update)
+ db = CouchObject::Database.open(location)
- # Returns a hash with the ID and Revision
- {:id => @id, :revision => @revision}
-
- else
+ json_value = self.to_json
+
+ unless (response = db.put(id, json_value)).to_document["error"]
+
+ @revision = response.to_document.revision
+
+ # If the user has activated smart_save, we should set the state
+ # to the new contents of this instance!
+ @couch_object_original_state = json_value if use_smart_save
+
+ perform_callback(:after_update)
+
+ # Returns a hash with the ID and Revision
+ {:id => @id, :revision => @revision}
- raise CouchObject::Errors::DatabaseSaveFailed, "The document couldn't be updated.\n" + \
- "The reason might be a revision number conflict.\n" + \
- "CouchDB reported: #{response.to_document["reason"]}"
+ else
+
+ raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
+ "couldn't be updated.\n" + \
+ "The reason might be a revision number conflict.\n" + \
+ "CouchDB reported: #{response.to_document["reason"]}"
+
+ end
+
end
+
end
-
+
public
#
+ # Classes WITH smart_save activated:
+ # Any instance should be able to know if it has unsaved changes or not.
+ # When an instance is loaded from the DB it creates a snapshot of what
+ # its variables contain. Based on a comparison between the snapshot
+ # and the contents of the instance this method returns true or false.
+ #
+ # Classes WITHOUT smart_save activated:
+ # Will always return true regardless of what state it is in
+ #
+ # A new object will always return true
+ #
+ # Returns:
+ # * true: if it has changes that haven't been saved to the database
+ # * fase: if the nothing has changed since it was loaded from the
+ # database.
+ #
+ def unsaved_changes?
+ return true if new?
+ return true unless use_smart_save
+
+ @couch_object_original_state == self.to_json ? false : true
+
+ end
+
+ #
# Any instance should be able to delete itself
#
# Takes:
@@ -572,7 +685,11 @@ module CouchObject
"@updated_at",
"@id",
"@revision",
- "@do_not_load_has_many_relations"]
+ "@do_not_load_has_many_relations",
+ "@do_not_load_belongs_to_relations",
+ "@couch_object_original_state",
+ "@couch_initial_load",
+ "@belongs_to"]
# We also have to remove all the objects that are related
# through belongs_to and has_many relations. They have to be called
@@ -608,23 +725,88 @@ module CouchObject
# If it is in belongs_to relationship(s), then that fact
# has to be stored in the database
- unless belongs_to == []
+ if belongs_to != [] and !@couch_initial_load # the couch_initial_load
+ # is to get the initial
+ # state of the object
+ # without loading the
+ # belongs_to relations
+ # which would start an
+ # infinite loop.
+ # NOTE: this is only for
+ # cases where smart_save
+ # has been activated.
+
+ # No reason to load the belongs_to relations if they haven't
+ # already been loaded from the database!
+ original_state_of_load_belongs_to_relations = \
+ @do_not_load_belongs_to_relations
+ @do_not_load_belongs_to_relations = true
+
what_it_belongs_to = {}
self.send(:belongs_to).each do |relation|
as_what = self.send("belongs_to_#{relation}_as")
object_it_belongs_to = self.send(relation)
-
+
# Unless the relation is unset, in which case it will be of
# type NilClass, set it.
- what_it_belongs_to[as_what] = object_it_belongs_to.id \
+ what_it_belongs_to[as_what.to_s] = object_it_belongs_to.id || "new" \
unless object_it_belongs_to.class == NilClass
end
- parameters["belongs_to"] = what_it_belongs_to
+
+ # Reset the value so they are loaded the next time when needed
+ # if that is what the user wants.
+ @do_not_load_belongs_to_relations = \
+ original_state_of_load_belongs_to_relations
+
+
+ # We have to make sure the @belongs_to variable contains all changes
+ # and all the original values for the keys that haven't changed/
+ # relations that haven't been loaded
+ original_belongs_to = @belongs_to || {}
+ @belongs_to = what_it_belongs_to
+ times_through = 1
+
+ # LOOP 1... see below for problem description
+ original_belongs_to.each_pair do |key, value|
+ times_through += 1
+ @belongs_to[key.to_s] = value unless @belongs_to[key.to_s]
+ end
+
+ # FIXME:
+ # Now... this is a really hacky way to solve this problem
+ # and should be improved... Feel free to come up with sollutions
+ # Case:
+ # If it has a belongs_to relationship that is new and therefore
+ # doesn't have an ID it would normally be written in the @belongs_to
+ # variable as nil. The problem is that if self has previously
+ # been saved with another parent object this ID would still come
+ # through in the @belongs_to variable updater (see "LOOP 1" above).
+ # We therefore assign the ID "new" to all unsaved relations, which we
+ # now have to nilify. If we don't the smart save wont work for this
+ # type of cases.
+
+ end
+
+ parameters["belongs_to"] = @belongs_to
+ parameters.delete("belongs_to") if @belongs_to == {} or @belongs_to == nil
+
+ # if @couch_initial_load && @belongs_to
+
+ begin
+ parameters.to_json
+ rescue JSON::GeneratorError
+ # All strings aren't encoded properly, so we have to force them into
+ # UTF-8.
+ # FIXME: The kconv library has some weird artefacts though where
+ # a lot of Norwegian (Scandianavian?) letters get turned into
+ # asian characters of some sort!
+ CouchObject::Utils::decode_strings(parameters).to_json
end
- parameters.to_json
end
+
+
#
# Sometimes you might want to add an object to
# a has_many relation without interacting with the other relations at all.
@@ -638,6 +820,26 @@ module CouchObject
def do_load_has_many_relations
@do_not_load_has_many_relations = false
end
+ alias do_not_load_has_one_relations do_not_load_has_many_relations
+ alias do_load_has_one_relations do_load_has_many_relations
+ alias do_not_load_has_one_relation do_not_load_has_many_relations
+ alias do_load_has_one_relation do_load_has_many_relations
+
+ #
+ # If you need to access the belongs_to variable without loading the
+ # relation if it hasn't already been loaded, you can call the instance
+ # method +do_not_load_belongs_to_relations+. To reactivate loading so
+ # the relation is loaded the next time it is needed, call the instance
+ # method +do_load_belongs_to_relations+.
+ #
+ def do_not_load_belongs_to_relations
+ @do_not_load_belongs_to_relations = true
+ end
+ def do_load_belongs_to_relations
+ @do_not_load_belongs_to_relations = false
+ end
+ alias do_not_load_has_many_relation do_not_load_has_many_relations
+ alias do_load_belongs_to_relation do_load_belongs_to_relations
protected
@@ -697,7 +899,7 @@ module CouchObject
db = CouchObject::Database.open(location)
- if (response = db.put("_design/#{view_name}/#{view_name}", \
+ if (response = db.put("_design%2F#{view_name}", \
view_code_query))
results = couch_load_has_many_relations(which_relation)
else
@@ -714,6 +916,8 @@ module CouchObject
#
def couch_load_belongs_to_relation(that_is_called)
+ return nil if new? || @do_not_load_belongs_to_relations
+
# If it doesn't have a belongs to ID then there is no
# related object in the database
if @belongs_to && @belongs_to[that_is_called] |