lib/couch_object/persistable.rb in couchobject-0.5.0 vs lib/couch_object/persistable.rb in couchobject-0.6.0

- old
+ new

@@ -1,59 +1,977 @@ +# = Persistable +# +# == Introduction +# +# The persistable mixin let's you save and load your classes +# to and from CoachDB using the instance method +save+ and +# the class level method +get_by_id+. +# Documentation and examples can be found in the README file. +# The specs also contain lots of examples. +# +# +# = TODOs and known issues: +# +# * 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 +# process has changed the content of the document in which case the local +# copy can't be stored to the DB. Should local changes be overwritten? +# How should differences between the version on the server and the local +# version be handeled? +# +# * ISSUE: +# When creating a new document the server does not return other values +# than the new documents ID and REVISION number. The local representation +# of the created_at and updated_at attributes are therefore set right before +# the call to the server is issued. Their values are therefore not +# guaranteed to be completely in sync with the values stored on the server! +# +# * ISSUE: +# When loading multiple objects from a view using the get_from_view +# mehtod that normally would have been in a belongs_to or has_many relation, +# their relations aren't set. +# +# * ISSUE: +# Given the following scenario: B is in a has_many/has_one relationship to A +# and both have been saved to the database. B is removed from the +# relationship and A is then saved. Saving A wont save the changed version +# of B where the relationship has been ended. Therefore: the next time A +# is loaded from the database B will be loaded as a relative! +# +# Possible sollutions: +# 1) keep a list of objects that need to be saved in A so that B gets +# saved when A is saved althouh the relationship has ended. +# 2) save B as it is removed from the relationship with A. +# +# I belive 1) to be the better solution as it doesn't prematurely save B +# in cases where you don't want the changes you do to be saved to the +# database. +# MAKE SURE: that the object that is added doesn't have the same id +# and revision number ass the object that is replaced. That happens +# when a class loads its belongs_to relations! In cases like that +# the old class should NOT be tracked and saved later! +# +$:.unshift File.dirname(__FILE__) +require 'persistable/has_many_relations_array' +require 'persistable/meta_classes' +require 'persistable/overloaded_methods' + module CouchObject - module Persistable - def self.included(klazz) - klazz.extend(ClassMethods) - end - + module Persistable + module ClassMethods - # Get a document from +db_uri+ with +id+ as the document id - def get_by_id(db_uri, id) - raise NoFromCouchMethodError unless respond_to?(:from_couch) + # + # Loads a document from the database + # + # Aliases: + # * +get+ + # + # Takes: + # * +id+: the ID of the document that should be loaded + # * +db_uri+: the uri to the database. Is optional if the database has + # been defined on class level: + # + # class SomeClass + # include CouchObject::Persistable + # database 'http://localhost:5984' + # end + # + # Returns: + # * a fully initialized class of type self + # + # Raises: + # * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+ + # is blank AND has not been set on class level + # * CouchObject::Errors::DocumentNotFound if the document doesn't + # exist or has been deleted + # + def get_by_id(id, db_uri = self.location) + # Raises an error if the location variable hasn't been set + raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri + db = CouchObject::Database.open(db_uri) - response = db.get(id) - self.send(:from_couch, response["attributes"]) + response = JSON.parse(db.get(id).body) + + if response["error"] + case response["reason"] + when "deleted" + raise CouchObject::Errors::DocumentNotFound, "The document has been deleted" + else + raise CouchObject::Errors::DocumentNotFound, "The document could not be found" + end + end + + # creates a new object and initialize all its sub objects + 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 + new_object.instance_variable_set("@location", db_uri) + + # return the new couch object + new_object end + + # + # Alias for get_by_id + # + 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 + # + # Takes: + # * +view+ (string): the name of the view to call + # * [+params+] (hash): a hash of URL query arguments supported + # by couchDB. If omitted it defaults to not use a key + # and not update the view. + # Additionally the +db_uri+ can be set as a parameter if + # it hasn't been defined at class level. + # + # Example: + # + # AppleTree.get_from_view("foo_view", + # { :db_uri => "http://localhost:5984/mydb", + # :update => false, :key => "bar"}) => Array + # + # Returns: + # * a array of initialized classes + # (if the view includes the documents full content) + # + # Raises: + # * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+ + # is blank AND has not been set on class level + # * CouchObject::Errors::MissingView if the view doesn't exist + # + def get_from_view( view, params = {:key => nil, + :update => true, + :db_uri => self.location}) + + # Raise an error if the location variable hasn't been set + db_uri = params[:db_uri] || self.location + raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri + + db = CouchObject::Database.open(db_uri) + + params.delete(:db_uri) + + #Create a querystring with the parameters passed inn + querystring = "?" + params.each_pair do |key, value| + querystring += \ + "#{key}=#{Utils.encode_querystring_parameter(value)}&" + end + querystring = querystring[0...-1] + + view_with_parameters = view + querystring + + objects_to_return = [] + + response = JSON.parse(db.get(view_with_parameters).body) + + raise CouchObject::Errors::MissingView, \ + "The view '#{view}' doesn't exist on the server" \ + if response["error"] == "not_found" + + raise CouchObject::Errors::CouchDBError, \ + "CouchDB returned and error and described the problem as #{response['reason']}. \n" + \ + "There might be something wrong with one of your views, or it might be missing!" \ + if response["error"] + + response["rows"].each do |params_for_object| + objects_to_return << couch_load_object(params_for_object["value"]) + end + + objects_to_return + + end + + protected + # + # This recursive method initializes new instances of self and + # makes sure all sub classes are also initialized. + # + # Takes: + # * A hash of parameters loaded from CoachDB + # + # Returns: + # * An fully initialized object + # + # Raises: + # * Does currently not raise any error + # + def couch_load_object(parameters) + + # Getting the values that shouldn't be passed on to the initializer + id = parameters["_id"] + revision = parameters["_rev"] + created_at = parameters["created_at"] + updated_at = parameters["updated_at"] + class_type = parameters["class"] + belongs_to = parameters["belongs_to"] + + new_object_from_couch = nil + + # Two possible routes: + # * the class has implemented a from_couch class method, in which + # case we use it, or + # * there is no from_couch method, so we just add the attributes + # as instance variables if they aren't part of a class... + if eval("#{class_type}.respond_to?(:from_couch)") + new_object_from_couch = eval("#{class_type}.send(:from_couch," \ + " parameters[\"attributes\"])") + else + new_object_from_couch = eval("#{class_type}.new") + unless parameters["attributes"] == nil + parameters["attributes"].each_key do |key| + # Check if the key value pair is a class + if parameters["attributes"][key].class == Hash && \ + parameters["attributes"][key]["class"] != nil + + new_object_from_couch.instance_variable_set("@#{key}", \ + self.couch_load_object(parameters["attributes"][key])) + else + # Add the value to the class + new_object_from_couch. + instance_variable_set("@#{key}", \ + parameters["attributes"][key]) + end + end + end + end + + # Sets couch_object related values + new_object_from_couch. + instance_variable_set("@revision", revision) if revision + + new_object_from_couch.instance_variable_set("@id", id) if id + + new_object_from_couch.instance_variable_set("@created_at", \ + created_at) if eval("#{class_type}." \ + "couch_object_timestamp_on_create?") + + new_object_from_couch.instance_variable_set("@updated_at", \ + updated_at) if eval("#{class_type}." \ + "couch_object_timestamp_on_update?") + + new_object_from_couch. + 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 + end end - # Save the object to +db_uri+ - def save(db_uri) - db = CouchObject::Database.open(db_uri) - response = db.post("", self.to_json) - unless response.empty? - @id = response["_id"] + public + + # + # Accessors for instance variables specific to the persistable + # CouchObject + # + attr_reader :id, + :revision, + :updated_at, + :created_at + + # + # Returns: + # * +true+ if the object hasn't been saved + # * +false+ if the object has previously been stored or is loaded from the + # document store + # + def new? + 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 - response end + + # + # Forces the instance object into smart save mode + # + def couch_force_smart_save + def self.use_smart_save + true + end + end - # Is this a new unsaved object? - def new? - id.nil? + # + # Saves the object to the db_uri supplied, or if not set, to the + # location the object has previously been saved to. + # + # Takes: + # * +db_uri+ as string which is the location of the database: + # Example: http://localhost:5984/mydb + # + # Raises: + # * CouchObject::Errors::NoDatabaseLocationSet error + # if the object doesn't have a previously set location and + # the +db_uri+ is nil + # + # Returns: + # * Hash with the id and revision: + # {:id => "1234", :revision => "ABC123"} + # + # Sub methods might raise: + # * CouchObject::Errors::DatabaseSaveFailed if the save fails + # + def save(db_uri = location) + # if the location hasn't been set, set it + @location ||= db_uri + + # Raises an error if the location variable hasn't been set + raise CouchObject::Errors::NoDatabaseLocationSet unless location + + # 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? + master_class.save + performed_save = true + 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 + + {:id => @id, :revision => @revision} + end + protected + def couch_perform_save + perform_callback(:before_save) + + # Go... action + the_return_value = new? ? couch_create : couch_update + + # Save all the has_many relations + # But only the has_many relations that have already + # been loaded! No need to load the relations from + # the db to save them back again! + 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| + 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| + related_object = self.send(thing_it_has_one_of) - # the Couch document id of this object - def id - @id + unless related_object == nil + related_object.save(location) + end + end + + # 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 end + + # + # Saves the class as a new document in the database + # + # Returns: + # * CouchObject::Response + # + # Raises: + # * CouchObject::Errors::DatabaseSaveFailed if the save fails + # + def couch_create + perform_callback(:before_create) + + db = CouchObject::Database.open(location) + + 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 + @created_at = Time.now if self. + class::couch_object_timestamp_on_create? + @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 + {:id => @id, :revision => @revision} - # serializes this object, based on its #to_couch method, into JSON + else + + raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \ + "couldn't be created.\n" + \ + "CouchDB reported: #{response.to_document["error"]}" + + end + end + + # + # Updates a document in the database based on the id and revision + # + # Returns: + # * CouchObject::Response + # + # Raises: + # * CouchObject::Errors::DatabaseSaveFailed if the update fails + # + def couch_update + + # Only save if it has unsaved changes! + if unsaved_changes? + + 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? + + db = CouchObject::Database.open(location) + + 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} + + 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: + # * +db_uri+ if not set in the location variable + # + # Returns: + # * true on success + # * false on failure + # + # Note: + # * it also deletes all has_many relations from the database + # * it removes itself from object it belongs to + # + # Raises: + # * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+ + # is blank AND has not been set on class level + # + def delete(db_uri = location) + + perform_callback(:before_delete) + + unless new? + # Raises an error if the location variable hasn't been set + raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri + + db = CouchObject::Database.open(db_uri) + + # Removes itself from the database + db.delete(id, revision) + end + + # Remove all relations + has_many.each do |what_it_has| + self.send(what_it_has).dup.each do |related_object| + related_object.delete + end + end + + # Remove the relationship with it's has many master object + belongs_to.each do |what_it_belongs_to| + self_belongs_to_classtype = what_it_belongs_to + + self_belongs_to_classtype_as = self. + send("belongs_to_#{what_it_belongs_to}_as") + + self_belongs_to_class = self.send(self_belongs_to_classtype) unless \ + self_belongs_to_classtype.nil? + + self_belongs_to_class.end_relationsship_with(self, + self_belongs_to_classtype_as) unless self_belongs_to_class.nil? + + # Remove the relationship with it's belongs_to master from itself + remove_call = "#{self_belongs_to_classtype.to_s}" + \ + "_without_call_back=" + self.send(remove_call.to_sym, nil) if self_belongs_to_classtype + + end + + # Reset itself + @id = nil + @revision = nil + @location = nil + + perform_callback(:after_delete) + + true + + end + + # + # Breaks relations if existing + # + # Takes: + # * +undesired_object+ as a reference to the object the + # relationship should be broken with + # * +which_is_stored_as+ (string) which is what the relation + # is stored as. + # + def end_relationsship_with(undersired_object, which_is_stored_as) + self.send(which_is_stored_as.to_sym).perform_remove(undersired_object) + end + + # + # Sets the location variable manually + # + # Takes: + # * +db_uri+ as string which is the location of the database: + # Example: http://localhost:5984/mydb + # + def set_location=(db_uri) + @location = db_uri == "" ? nil : db_uri + end + alias set_storage_location= set_location= + + # + # serializes this object into JSON + # + # Returns: + # * The values of the class in json format + # + # Example + # {"class":"Bike","attributes":{"wheels":2}} + # def to_json - raise NoToCouchMethodError unless respond_to?(:to_couch) - {"class" => self.class, "attributes" => self.to_couch}.to_json + parameters = {} + + parameters["class"] = self.class + + if respond_to?(:to_couch) + parameters["attributes"] = self.to_couch + else + # Find all the instance variables and add them to the + # attributes parameter + p_attributes = {} + instance_variables = \ + self.instance_variables - ["@location", + "@created_at", + "@updated_at", + "@id", + "@revision", + "@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 + # saved separately + has.each do |thing_it_has| + instance_variables = instance_variables - \ + ["@couch_object_#{thing_it_has.to_s}"] + end + + belongs_to.each do |things_it_belongs_to| + instance_variables = instance_variables - \ + ["@couch_object_#{things_it_belongs_to.to_s}"] + end + + instance_variables.each do |var| + p_attributes[var[1..(var.length)]] = self.instance_variable_get(var) + end + parameters["attributes"] = p_attributes + end + + parameters["updated_at"] = Time.now \ + if self.class::couch_object_timestamp_on_update? + + unless new? + parameters["_id"] = id + parameters["_rev"] = revision + parameters["created_at"] = created_at \ + if self.class::couch_object_timestamp_on_create? + else + parameters["created_at"] = Time.now \ + if self.class::couch_object_timestamp_on_create? + end + + # If it is in belongs_to relationship(s), then that fact + # has to be stored in the database + 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.to_s] = object_it_belongs_to.id || "new" \ + unless object_it_belongs_to.class == NilClass + end + + # 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 + end - class NoToCouchMethodError < StandardError - def message - "You need to define a #to_couch method that returns a hash of the " + - "attributes you want to persist" + + + # + # Sometimes you might want to add an object to + # a has_many relation without interacting with the other relations at all. + # In cases like that, when loading all the relations would just + # cause unnecessary traffic to the database, you can tell the object + # not to load it has_many relations using this method + # + def do_not_load_has_many_relations + @do_not_load_has_many_relations = true + end + 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 + # + # Loads has_many relations + # + def couch_load_has_many_relations(which_relation) + # If it is a new and unsaved object it wont have + # relations in the DB. Return a blank array + results = CouchObject::Persistable::HasManyRelation.new(self) + + # If it is loading a has_one relation, the relation name has to + # be changed + which_relation = which_relation[8..-1] \ + if which_relation[0..7] == "has_one_" + + return results if new? || @do_not_load_has_many_relations + + view_name = "couch_object_has_many_relations" + + begin + + new_objects = self.class. + get_from_view("_view/#{view_name}/related_documents", \ + {:key => [self.id, which_relation], + :db_uri => location} ) + + # Disable the call back function of HasManyRelations so we don't + # get an infinite loop + results.disable_call_back_on_add + + new_objects.each do |new_object| + results << new_object + end + + # Reanable the call back function again so it works like normal + # from now on + results.enable_call_back_on_add + + rescue CouchObject::Errors::MissingView, CouchObject::Errors::CouchDBError + + # The view doesn't exist. It means this is the first + # time this script is used for a given database, or the user + # has deleted the view + view_code_query = JSON.unparse( + { + "_id" => "_design/#{view_name}", + "language" => "text/javascript", + "views" => + { + "related_documents" => "function(doc){if (doc.belongs_to){" + \ + "for (var i in doc.belongs_to) {map([doc.belongs_to[i],i], " + \ + "doc);}}}" + } + } + ) + + db = CouchObject::Database.open(location) + + if (response = db.put("_design%2F#{view_name}", \ + view_code_query)) + results = couch_load_has_many_relations(which_relation) + else + raise CouchObject::Errors::StandardError, "Couldn't create the view..." + end end - alias_method :to_s, :message + + return results end - class NoFromCouchMethodError < StandardError - def message - "You need to define a from_couch(attrs) class method that maps attrs " + - "to your class instance" + # + # Loads the belongs_to relation if the class has a previously + # save relation + # + 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] + + # If it is a new and unsaved object it wont have + # relations in the DB. Return a blank array + return nil if new? + + # Raises an error if the location variable hasn't been set + raise CouchObject::Errors::NoDatabaseLocationSet unless location + + db = CouchObject::Database.open(location) + + class_self_belongs_to = self. + class.get_by_id(@belongs_to[that_is_called]) + + # We have to add self to the other end of the relation + # Adds itself to the relationship + masters_relation = class_self_belongs_to.send(that_is_called) + + if masters_relation.class == CouchObject::Persistable::HasManyRelation + # it is a has_many relations + # remove the object in the relation that is the freshly loaded + # copy of self, and then add self instead. + masters_relation.each do |has_one| + if has_one.id == self.id && has_one.revision == self.revision + masters_relation.perform_remove(has_one) + break + end + end + masters_relation.disable_call_back_on_add + masters_relation << self + masters_relation.enable_call_back_on_add + else + # it is a has_one relation, just add itself + class_self_belongs_to.send("#{that_is_called}=".to_sym, self) + end + + return class_self_belongs_to + + else + nil end - alias_method :to_s, :message end + + # + # Performs callbacks before and after these events: + # * create + # * update + # * save + # * delete + # + def perform_callback(the_callback) + self.send(the_callback) if self.respond_to?(the_callback) + end end end \ No newline at end of file