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