require 'facet/classinherit' require 'facet/kernel/assign_with' require 'glue/property' require 'og/relation' require 'og/ez/clause' require 'og/ez/condition' module Og # Include this module to classes to make them managable by Og. #-- # gmosx, WARTNING: If you change the methods here, don't # forget to update the Cacheable overrides. #++ module EntityMixin def self.included(base) # If the after_enchant callback is defined, call it # to allow for some customization. Have a look at cacheable # for an example. if base.respond_to?(:after_enchant) base.after_enchant(base) end end # Persist the object. def save(options = nil) self.class.ogmanager.store.save(self, options) end alias_method :save!, :save alias_method :validate_and_save, :save # Force saving of the objects, even if the validations # don't pass. def force_save!(options = nil) self.class.ogmanager.store.force_save(self, options) end # Insert the object in the store. def insert self.class.ogmanager.store.insert(self) return self end # Update an existing object in the store. def update(options = nil) self.class.ogmanager.store.update(self, options) end def update_properties(*properties) self.class.ogmanager.store.update(self, :only => properties) end alias_method :update_property, :update_properties alias_method :pupdate, :update_properties def update_by_sql(set) self.class.ogmanager.store.update_by_sql(self, set) end alias_method :update_sql, :update_by_sql alias_method :supdate, :update_by_sql # Reload this entity instance from the store. def reload self.class.ogmanager.store.reload(self, self.pk) end alias_method :reload!, :reload # Delete this entity instance from the store. def delete(cascade = true) self.class.ogmanager.store.delete(self, self.class, cascade) end alias_method :delete!, :delete def transaction(&block) self.class.ogmanager.store.transaction(&block) end # Is this object saved in the store? def saved? not @oid.nil? end alias_method :serialized?, :saved? def assign_properties(values, options = {}) Property.populate_object(self, values, options) return self end alias_method :assign, :assign_properties # Returns a symbol => value hash of the object's # properties. def properties_to_hash hash = {} for sym, prop in self.class.properties hash[sym] = instance_variable_get("@#{sym}") end end def og_quote(obj) self.class.ogmanager.store.quote(obj) end def og_clone(*args) Og::Entity.clone(self,*args) end include RelationDSL class_inherit do def create(*args) obj = self.new(*args) yield(obj) if block_given? ogmanager.store.save(obj) return obj end # An alternative creation helper, only works # with objects that have an initialize method # tha works with no arguments. def create_with(hash) obj = self.new obj.assign_with(hash) ogmanager.store.save(obj) return obj end def assign_properties(values, options = {}) Property.populate_object(self.new, values, options) end alias_method :assign, :assign_properties # Load an instance of this Entity class using the primary # key. def load(pk) ogmanager.store.load(pk, self) end alias_method :[], :load alias_method :exist?, :load # Update the representation of this class in the # store. def update(set, options = nil) ogmanager.store.update_by_sql(self, set, options) end # Find a specific instance of this class according # to the given conditions. # # Unlike the lower level store.find method it accepts # Strings and Arrays instead of an option hash. # # === Examples # # User.find :condition => "name LIKE 'g%'", :order => 'name ASC' # User.find :where => "name LIKE 'g%'", :order => 'name ASC' # User.find :sql => "WHERE name LIKE 'g%' ORDER BY name ASC" # User.find :condition => [ 'name LIKE ?', 'g%' ], :order => 'name ASC', :limit => 10 # User.find "name LIKE 'g%'" # User.find "WHERE name LIKE 'g%' LIMIT 10" # User.find [ 'name LIKE ?', 'g%' ] def find(options = {}, &block) options = resolve_non_hash_options(options) ez_resolve_options(options, &block) if block_given? if find_options = self.ann.self[:find_options] options = find_options.dup.update(options) end options[:class] = self options[:type] = self if self.schema_inheritance_child? ogmanager.store.find(options) end alias_method :all, :find # Find a single instance of this class. def find_one(options = {}, &block) options = resolve_non_hash_options(options) ez_resolve_options(options, &block) if block_given? if find_options = self.ann.self[:find_options] options = find_options.dup.update(options) end options[:class] = self options[:type] = self if self.schema_inheritance_child? ogmanager.store.find_one(options) end alias_method :one, :find_one alias_method :first, :find_one # Select an object using an sql query. def select(sql) ogmanager.store.select(sql, self) end # Select one instance using an sqll query. def select_one(sql) ogmanager.store.select_one(sql, self) end # :section: Aggregations / Calculations # Perform a general aggregation/calculation. def aggregate(term, options = {}) options[:class] = self ogmanager.store.calculate(term, options) end alias_method :calculate, :aggregate # Perform a count query. def count(options = {}) calculate('COUNT(*)', options).to_i end # Find the minimum of a property. # Pass a :group option to return an aggregation. def minimum(min, options = {}) calculate("MIN(#{min})", options) end alias_method :min, :minimum # Find the maximum of a property. # Pass a :group option to return an aggregation. def maximum(max, options = {}) calculate("MAX(#{max})", options) end alias_method :max, :maximum # Find the average of a property. # Pass a :group option to return an aggregation. def average(avg, options = {}) calculate("AVG(#{avg})", options) end alias_method :avg, :average # Find the sum of a property. # Pass a :group option to return an aggregation. def summarize(sum, options = {}) calculate("SUM(#{sum})", options) end alias_method :sum, :summarize # :section: Delete/Destroy methods. # Delete an instance of this Entity class using the actual # instance or the primary key. def delete(obj_or_pk, cascade = true) ogmanager.store.delete(obj_or_pk, self, cascade) end alias_method :delete!, :delete # Delete all objects of this Entity class. #-- # TODO: add cascade option. #++ def delete_all ogmanager.store.delete_all(self) end def destroy ogmanager.store.send :destroy, self end def escape(str) ogmanager.store.escape(str) end def transaction(&block) ogmanager.store.transaction(&block) end # Return the store (connection) for this class. def ogstore ogmanager.store end # Returns the primary key for this class. def primary_key # gmosx: LEAVE as is, seems to be a workaround for a # nasty bug in the current facets version. pk = ann.self.primary_key if pk.nil? pk = Entity.resolve_primary_key(self) ann :self, :primary_key => pk end return pk end # Set the default find options for this entity. def set_find_options(options) ann self, :find_options => options end alias_method :find_options, :set_find_options # Enable schema inheritance for this Entity class. # The Single Table Inheritance pattern is used. def set_schema_inheritance include Og::SchemaInheritanceBase end alias_method :schema_inheritance, :set_schema_inheritance def schema_inheritance? ancestors.include? Og::SchemaInheritanceBase end def schema_inheritance_child? schema_inheritance? and superclass.respond_to?(:schema_inheritance?) end def schema_inheritance_root? schema_inheritance? and (!superclass.respond_to?(:schema_inheritance?)) end #-- # farms/rp: is there not another way to access the root class? #++ def schema_inheritance_root_class klass = self until !Og.manager.manageable?(klass) or klass.schema_inheritance_root? klass = klass.superclass end return klass end # Set the default order option for this entity. def set_order(order_str) unless ann.self.find_options.nil? ann.self.find_options[:order] = order_str else ann self, :find_options => { :order => order_str } end end alias_method :order, :set_order alias_method :order_by, :set_order # Set a custom table name. def set_sql_table(table) ann self, :sql_table => table.to_s end alias_method :set_table, :set_sql_table # Set the primary key. def set_primary_key(pk, pkclass = Fixnum) #ann self, :primary_key => Property.new(:symbol => pk, :klass => pkclass) ann self, :primary_key => properties[pk].dup end # Is this entity a polymorphic parent? def polymorphic_parent? self.to_s == self.ann.self.polymorphic.to_s end # Used internally to fix the forward reference problem. def const_missing(sym) # :nodoc: all return sym end # Returns an array of all relations formed by other og managed # classes with the class of this object. # # This is needed by the PostgreSQL foreign key constraints # system. def resolve_remote_relations klass = self manager = klass.ogmanager relations = Array.new manager.managed_classes.each do |managed_class| next if managed_class == klass managed_class.relations.each do |rel| relations << rel if rel.target_class == klass end end relations end # Define a scope for the following og method invocations # on this managed class. The scope options are stored # in a thread variable. # # At the moment the scope is only considered in find # queries. def set_scope(options) Thread.current["#{self}_scope"] = options end # Get the scope. def get_scope Thread.current["#{self}_scope"] end # Execute some Og methods in a scope. def with_scope(options) set_scope(options) yield set_scope(nil) end # Handles dynamic finders. Handles methods such as: # # class User # property :name, String # property :age, Fixnum # end # # User.find_by_name('tml') # User.find_by_name_and_age('tml', 3) # User.find_all_by_name_and_age('tml', 3) # User.find_all_by_name_and_age('tml', 3, :name_op => 'LIKE', :age_op => '>', :limit => 4) # User.find_or_create_by_name_and_age('tml', 3) #-- # TODO: refactor this method. #++ def method_missing(sym, *args) if match = /find_(all_by|by)_([_a-zA-Z]\w*)/.match(sym.to_s) return finder(match, args) elsif match = /find_or_create_by_([_a-zA-Z]\w*)/.match(sym.to_s) obj = finder(match, args) unless obj attrs = match.captures.last.split('_and_') obj = self.create do |obj| attrs.zip(args).map do |name, value| obj.instance_variable_set "@#{name}", value end end yield(obj) if block_given? end return obj else super end end private # Resolve String/Array options. #-- # FIXME: move to sql store? #++ def resolve_non_hash_options(options) if options.is_a? String if options =~ /^WHERE/i # pass the string as sql. return { :sql => options } else # pass the string as a condition. return { :condition => options } end elsif options.is_a? Array # pass the array as condition (prepared statement style # parsing/quoting. return { :condition => options } end return options end # Resolve ez options, ie options provided using the # Ruby query language. #-- # gmosx: investigate this. #++ def ez_resolve_options(options, &block) klass = self.name.downcase.to_sym # conditions on self first # conditions = [ez_condition(:outer => outer_mapping[klass], :inner => inner_mapping[klass])] conditions = [ez_condition()] # options[:include].uniq.each do |assoc| # conditions << reflect_on_association(assoc).klass.ez_condition(:outer => outer_mapping[assoc], :inner => inner_mapping[assoc]) # end yield *conditions condition = Caboose::EZ::Condition.new conditions.each { |c| condition << c } options[:condition] = condition.to_sql # p options[:conditions] if $DEBUG end # Returns a Condition for this object. def ez_condition(options = {}, &block) options[:table_name] ||= table() Caboose::EZ::Condition.new(options, &block) end # Helper method for dynamic finders. Finds the object dynamically parsed # method name is after. def finder(match, args) finder = (match.captures.first == 'all_by' ? :find : :find_one) attrs = match.captures.last.split('_and_') options = (annotation[:find_options] || {}).dup options = args.pop if args.last.is_a?(Hash) relations_map = relations.map{|r| [r.name.to_s,r]} condition = attrs.zip(args).map do |name, value| if relation = relations_map.assoc(name) relation = relation.last field_name = relation.foreign_key value = value.send(relation.target_class.primary_key.symbol) else field_name = properties[name.to_sym][:field] || properties[name.to_sym][:name] || properties[name.to_sym][:symbol] end options["#{name}_op".to_sym] = 'IN' if value.is_a?(Array) %|#{field_name} #{options.delete("#{name}_op".to_sym) || '='} #{ogmanager.store.quote(value)}| end.join(' AND ') options.merge!( :class => self, :condition => condition ) return ogmanager.store.send(finder, options) end end end # An Og Managed class. Also contains helper # methods. class Entity include EntityMixin class << self def resolve_primary_key(klass) # Is the class annotated with a primary key? if pk = klass.ann.self[:primary_key] return pk end # Search the properties, try to find one annotated as primary_key. for p in klass.properties.values if p.primary_key return Property.new(:symbol => p.symbol, :klass => p.klass) end end # The default primary key is oid. return Property.new(:symbol => :oid, :klass => Fixnum) end # Converts a string into it's corresponding class. Added to support STI. # Ex: x = "Dave" becomes: (x.class.name == Dave) == true. # Returns nil if there's no such class. #-- # gmosx: investigate this patch! #++ def entity_from_string(str) res = nil Og.manager.managed_classes.each do |klass| if klass.name == str res = klass break end end res end # Entity copying support. Eventually this should all # be eval'd in at enchanting stage for the minor # speed increase. # TODO: Convert to enchantments on objects # Accepts source object, destination and ignore. # Source and destination are self explanatory; ignore # is a list of properties not to copy (i.e. # :create_time,:update_time). # By default sets the class variables directly on the # remote model instance, if you set use_setter_method to # true, uses create_time= style copying tactics, def copy_properties(source, destination, ignore = [], use_setter_method = false) property_copier(source, destination, ignore, use_setter_method, false) end # Copies relations of one record to another. Only copies # has_one, refers_to, belongs_to relationships as # has_many requires modifying of other objects and # cannot be copied (by design). If you think you need to copy # these relations, what you need is a joins_many relationship # which can be copied. def copy_inferior_relations(source, destination, ignore = []) real_ignore = Array.new # Map relation symbols to foreign keys. ignore.each do |symbol| source.class.relations.reject{|r| [Og::JoinsMany, Og::ManyToMany, Og::HasMany].include?(r.class)}.each do |relation| if relation.name == symbol.to_s real_ignore << relation.foreign_key.to_sym break end end end # Use instance variable property copier method. property_copier(source, destination, real_ignore, false, true) end def copy_equal_relations(source, destination, ignore = []) source.class.relations.reject{|r| not [Og::JoinsMany, Og::ManyToMany].include?(r.class)}.each do |relation| next if relation.name == nil or ignore.include?(relation.name) source.send(relation.name).each do |related| destination.send(relation.name).send(:<<, related) end end end # Copies all relations *except* HasMany which is impossible # to copy. Use a JoinsMany relation instead if you need a # copyable HasMany (which is irrational). def copy_relations(source, destination, ignore = []) copy_inferior_relations(source, destination, ignore) copy_equal_relations(source, destination, ignore) end # Clones an object in every possible way (cannot copy # HasMany but can copy all others - BelongsTo, etc). # Provide a source object as first arguments, the rest # (if any) are passed along to the initialize constructor # when calling new to make the copied object. def clone(source,*args) destination = source.class.new(*args) copy_properties(source, destination, [], false) # Must save here to copy join tables. destination.save! copy_relations(source, destination, []) destination.save! destination end # Does the work of clone_properties and copy_inferior_relations. # Syntax is the same with one extra field to tell the # routine what it is copying. def property_copier(source,destination,ignore,use_setter_method,relations) primary_key_symbol = source.class.primary_key.symbol source.class.properties.to_a.each do |symbol, property| next if primary_key_symbol == symbol or ignore.include?(symbol) or (relations and not property.relation) or (not relations and property.relation) variable = "@#{symbol}" if use_setter_method destination.send("#{symbol}=".to_sym,source.instance_variable_get(variable)) else destination.instance_variable_set(variable, source.instance_variable_get(variable)) end end end end end end # * George Moschovitis # * Tom Sawyer # * Rob Pitt