require 'data_mapper/property' require 'data_mapper/attributes' require 'data_mapper/support/serialization' require 'data_mapper/validations' require 'data_mapper/associations' require 'data_mapper/callbacks' require 'data_mapper/embedded_value' require 'data_mapper/auto_migrations' require 'data_mapper/dependency_queue' require 'data_mapper/support/struct' module DataMapper # See DataMapper::Persistence::ClassMethods for DataMapper's DSL documentation. module Persistence class IncompleteModelDefinitionError < StandardError end # This probably needs to be protected attr_accessor :loaded_set include Comparable def self.included(klass) klass.extend(ClassMethods) klass.extend(ConvenienceMethods::ClassMethods) klass.send(:include, ConvenienceMethods::InstanceMethods) klass.send(:include, Attributes) klass.send(:include, Associations) klass.send(:include, Validations) klass.send(:include, CallbacksHelper) klass.send(:include, Support::Serialization) klass.instance_variable_set('@properties', []) klass.send :extend, AutoMigrations klass.subclasses DataMapper::Persistence::subclasses << klass unless klass == DataMapper::Base klass.send(:undef_method, :id) if method_defined?(:id) # When this class is sub-classed, copy the declared columns. klass.class_eval do def self.subclasses @subclasses || (@subclasses = Support::TypedSet.new(Class)) end def self.inherited(subclass) super_table = database.table(self) if super_table.type_column.nil? super_table.add_column(:type, :class, {}) end subclass.instance_variable_set('@properties', self.instance_variable_get("@properties").dup) subclass.instance_variable_set("@callbacks", self.callbacks.dup) self::subclasses << subclass end def self.persistent? true end end end # Migrates the database schema based on the properties defined within # models. This includes removing fields no longer listed in models and # adding new ones. # # This is destructive. Any data stored in the database will be destroyed # when this method is called. # # ==== Returns # True:: successfully automigrated database # False:: an error occured when automigrating the database # # @public def self.auto_migrate! subclasses.each do |subclass| subclass.auto_migrate! end end # Drops all tables known by the schema # # ==== Returns # True:: successfully automigrated database # False:: an error occured when automigrating the database # # @public def self.drop_all_tables! database.adapter.schema.each do |table| table.drop! end end # Track classes that include this module. # ==== Returns # Support::TypedSet:: # contains classes that include or inherit from this module # # @semipublic def self.subclasses @subclasses || (@subclasses = Support::TypedSet.new(Class)) end # Track classes that include this module. # ==== Returns # Support::TypedSet:: # contains classes that include or inherit from this module # # @semipublic def self.dependencies @dependency_queue || (@dependency_queue = DependencyQueue.new) end def initialize(details = nil) check_for_properties! if details initialize_with_attributes(details) end end def initialize_with_attributes(details) case details when Hash then self.attributes = details when details.respond_to?(:persistent?) then self.private_attributes = details.attributes when Struct then self.private_attributes = details.attributes end end def check_for_properties! raise IncompleteModelDefinitionError.new("Models must have at least one property to be initialized.") if self.class.properties.empty? end module ConvenienceMethods module InstanceMethods # Save updated properties to the database. # # ==== Returns # True:: successfully saved the object to the database # False:: an error occured when saving the object to the database. # check valid? to see if validation error occured # # @public def save database_context.save(self) end # This behaves in the same way as save, but raises a ValidationError # if the model is invalid. Successful saves return true. # # ==== Returns # True:: successfully saved the object to the database # # ==== Raises # ValidationError:: # The object could not be saved to the database due to validation # errors # @public def save! raise ValidationError.new(errors) unless save return true end # Reloads a model's properties from the database. This also includes # data for any associated models that have been loaded from the # database. # # You can limit the properties being reloaded by passing in an array # of symbols. def reload!(cols = nil) database_context.first(self.class, key, :select => ([self.class.table.key.to_sym] + (cols || original_values.keys)).uniq, :reload => true) self.loaded_associations.each { |association| association.reload! } self end alias reload reload! # Deletes the model from the database and de-activates associations def destroy! database_context.destroy(self) end end module ClassMethods # Attempts to find an object using options passed as # search_attributes, and falls back to creating the object if it # can't find it. # # ==== Parameters # search_attributes :: # attributes used to perform the search, and which can be later # merged with create_attributes when creating a record # create_attributes :: # attributes which are merged into the search_attributes when a # record is unfound and needs to be created # # ==== Returns # Object:: the found or created object from the database # # ==== Raises # ValidationError:: # An object was not found, and could not be created due to errors # in validation. # DataObject::QueryError:: # The database threw an error # - # @public def find_or_create(search_attributes, create_attributes = {}) first(search_attributes) || create(search_attributes.merge(create_attributes)) end # returns an array of objects matching options. # # ==== Parameters # options :: # hash of parameters to search by # # ==== Returns # Array:: contains all matched objects from the database, or an # empty set # # ==== Options # Basics: # Widget.all # => no conditions # Widget.all :order => 'created_at desc' # => ORDER BY created_at desc # Widget.all :limit => 10 # => LIMIT 10 # Widget.all :offset => 100 # => OFFSET 100 # Widget.all :include => [:gadgets] # => performs the JOIN according to # its association with Gadgets # # Any non-standard options are assumed to be column names and are ANDed together: # Widget.all :age => 10 # => WHERE age = 10 # Widget.all :age => 10, :title => 'Toy' # => WHERE age = 10 AND title = 'Toy' # # Using Symbol Operators[link:classes/DataMapper/Support/Symbol/Operator.html]: # Widget.all :age.gt => 20 # => WHERE age > 10 # Widget.all :age.gte => 20, :name.like => '%Toy%' # => WHERE age >= 10 and name like '%Toy%' # # Variations of syntax include the :conditions => {} as well as interpolated arrays # Widget.all :conditions => {:age => 10} # => WHERE age = 10 # Widget.all :conditions => ["age = ?", 10] # => WHERE age = 10 # # Syntaxes can be mixed-and-matched as well # Widget.all :conditions => ["age = ?", 10], :title => 'Toy' # # => WHERE age = 10 AND title = 'Toy' # # ==== Raises # DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError:: # A query could not be constructed from the hash passed in as # options # DataObject::QueryError:: # The database threw an error # - # @public def all(options = {}) database.all(self, options) end # Allows you to iterate over a collection of matching records. The # first argument is the find options. The second is a block that will # be called for every matching record. # # The valid options are the same as those documented in #all, # except the :offset option, which is not allowed. def each(options = {}, &b) raise ArgumentError.new(":offset is not supported with the #each method") if options.has_key?(:offset) offset = 0 limit = options[:limit] || (self::const_defined?('DEFAULT_LIMIT') ? self::DEFAULT_LIMIT : 500) until (results = all(options.merge(:limit => limit, :offset => offset))).empty? results.each(&b) offset += limit end end # Returns the first object which matches the query generated from the arguments # # ==== Parameters # see all() # # ==== Returns # Object:: first object from the database which matches the query # nil:: no object could be found which matches the query # # ==== Raises # DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError:: # A query could not be generated from the arguments passed in # DataObject::QueryError:: # The database threw an error # - # @public def first(*args) database.first(self, *args) end # returns the count of rows that match the given options hash. See # all() for a list of possible arguments. # NOTE: discards :offset, :limit, :order # # ==== Parameters # see all(). # # ==== Returns # Integer:: number of rows matching query # # ==== Raises # DataMapper::Adapters::Sql::Commands::LoadCommand::ConditionsError:: # A query could not be generated from the arguments passed in # DataObject::QueryError:: # The database threw an error # - # @public def count(*args) database.count(self, *args) end # Does what it says. Deletes all records in a model's table. # before_destroy and after_destroy callbacks are called and # paranoia is respected. # # ==== Returns # nil:: successfully deleted all rows # # ==== Raises # DataObject::QueryError:: # The database threw an error # - # @public def delete_all database.delete_all(self) end def truncate! database.truncate(self) end # This method allows for ActiveRecord style queries. The first # argument is a symbol indicating a search for a single record or a # collection — :first and :all respectively. The # second argument is the hash of options for your query. For a list # of valid options, please refer to the #all method. # # Widget.find(:all, :active => true) # => An array of active widgets # Widget.find(:first, :active => true) # => The first active widget found def find(type_or_id, options = {}) case type_or_id when :first then first(options) when :all then all(options) else first(type_or_id, options) end end # supply this method with the full SQL you wish to search on, and it # will return an array of Structs with your results set in them. # # NOTE: this does NOT return objects of a specific type, but rather # Struct objects with as many attributes as what you requested in # your full SQL query. These structs are read-only. # # If you only indicate you want 1 specific column, Datamapper and # DataObjects will do their best to type-cast the result as best they # can, rather than supplying you with an array of length 1 containing # Structs with 1 attribute. def find_by_sql(*args) DataMapper::database.query(*args) end # finds a single row from the database by it's primary key. # If you declared a property with :key => true, it's safe to # use here. # Example: # Widget.get(100) # => widget with the primary key of 100 # Widget.get('Toy') # => widget with the primary natural key of 'Toy' def get(*keys) database.get(self, keys) end # synonym for get() # ==== Parameters # keys :: keys which which to look up objects in the table. # # ==== Returns # object :: object matching the request # # ==== Raises # DataMapper::ObjectNotFoundError # could not find the object requested # - # @public def [](*keys) # Eventually this ArgumentError should be removed. It's only here # to help # migrate users away from the [options_hash] syntax, which is no # longer supported. raise ArgumentError.new('Hash is not a valid key') if keys.size == 1 && keys.first.is_a?(Hash) instance = database.get(self, keys) raise ObjectNotFoundError.new() unless instance return instance end # creates (and saves) a new instance of the object. def create(attributes) instance = self.new_with_attributes(attributes) instance.save instance end # the same as create(), though will raise an ObjectNotFoundError if # the instance could not be saved def create!(attributes) instance = create(attributes) raise ObjectNotFoundError.new(instance) if instance.new_record? instance end end end module ClassMethods def new_with_attributes(details) instance = allocate instance.initialize_with_attributes(details) instance end # Track classes that include this module. def subclasses @subclasses || (@subclasses = []) end def logger database.logger end def transaction yield end # The foreign key for a model. It is based on the lowercased and # underscored name of the class, suffixed with _id. # # Widget.foreign_key # => "widget_id" # NewsItem.foreign_key # => "news_item_id" def foreign_key Inflector.underscore(self.name) + "_id" end def extended(klass) end def table database.table(self) end # Adds property accessors for a field that you'd like to be able to # modify. The DataMapper doesn't # use the table schema to infer accessors, you must explicity call # #property to add field accessors # to your model. # # Can accept an unlimited amount of property names. Optionally, you may # pass the property names as an # array. # # For more documentation, see Property. # # EXAMPLE: # class CellProvider # property :name, :string # property :rating_number, :rating_percent, :integer # will create two properties with same type and text # property [:bill_to, :ship_to, :mail_to], :text, :lazy => false # will create three properties all with same type and text # end # # att = CellProvider.new(:name => 'AT&T') # att.rating = 3 # puts att.name, att.rating # # => AT&T # => 3 # # OPTIONS: # * lazy: Lazy load the specified property (:lazy => true). False by default. # * accessor: Set method visibility for the property accessors. Affects both # reader and writer. Allowable values are :public, :protected, :private. Defaults to # :public # * reader: Like the accessor option but affects only the property reader. # * writer: Like the accessor option but affects only the property writer. # * protected: Alias for :reader => :public, :writer => :protected # * private: Alias for :reader => :public, :writer => :private def property(*columns_and_options) columns, options = columns_and_options.partition {|item| not item.is_a?(Hash)} options = (options.empty? ? {} : options[0]) type = columns.pop @properties ||= [] new_properties = [] columns.flatten.each do |name| property = DataMapper::Property.new(self, name, type, options) new_properties << property @properties << property end return (new_properties.length == 1 ? new_properties[0] : new_properties) end # TODO: Figure out how to make EmbeddedValue work with new property # code. EV relies on these next two methods. def property_getter(mapping, visibility = :public) if mapping.lazy? class_eval <<-EOS #{visibility.to_s} def #{mapping.name} lazy_load!(#{mapping.name.inspect}) class << self; attr_accessor #{mapping.name.inspect} end @#{mapping.name} end EOS else class_eval("#{visibility.to_s}; def #{mapping.name}; #{mapping.instance_variable_name} end") unless [ :public, :private, :protected ].include?(mapping.name) end if mapping.type == :boolean class_eval("#{visibility.to_s}; def #{mapping.name.to_s.ensure_ends_with('?')}; #{mapping.instance_variable_name} end") end rescue SyntaxError raise SyntaxError.new(mapping) end def property_setter(mapping, visibility = :public) if mapping.lazy? class_eval <<-EOS #{visibility.to_s} def #{mapping.name}=(value) class << self; attr_accessor #{mapping.name.inspect} end @#{mapping.name} = value end EOS else class_eval("#{visibility.to_s}; def #{mapping.name}=(value); #{mapping.instance_variable_name} = value end") end rescue SyntaxError raise SyntaxError.new(mapping) end # Allows you to override the table name for a model. # EXAMPLE: # class WorkItem # set_table_name 't_work_item_list' # end def set_table_name(value) database.table(self).name = value end # An embedded value maps the values of an object to fields in the # record of the object's owner. # #embed takes a symbol to define the embedded class, options, and # an optional block. See # examples for use cases. # # EXAMPLE: # class CellPhone < DataMapper::Base # property :number, :string # # embed :owner, :prefix => true do # property :name, :string # property :address, :string # end # end # # my_phone = CellPhone.new # my_phone.owner.name = "Nick" # puts my_phone.owner.name # # => Nick # # OPTIONS: # * prefix: define a column prefix, so instead of mapping # :address to an 'address' column, it would map to # 'owner_address' in the example above. If # :prefix => true is specified, the prefix will # be the name of the symbol given as the first # parameter. If the prefix is a string the # specified # string will be used for the prefix. # * lazy: lazy-load all embedded values at the same time. # :lazy => true to enable. Disabled (false) by # default. # * accessor: Set method visibility for all embedded # properties. Affects both reader and writer. # Allowable values are :public, :protected, # :private. Defaults to :public # * reader: Like the accessor option but affects only # embedded property readers. # * writer: Like the accessor option but affects only # embedded property writers. # * protected: Alias for :reader => :public, # :writer => :protected # * private: Alias for :reader => :public, :writer => :private # def embed(name, options = {}, &block) EmbeddedValue::define(self, name, options, &block) end # Returns the hash of properties for this model. def properties @properties end # Creates a composite index for an arbitrary number of database columns. # Note that it also is possible to specify single indexes directly for # each property. # # === EXAMPLE WITH COMPOSITE INDEX: # class Person < DataMapper::Base # property :server_id, :integer # property :name, :string # # index [:server_id, :name] # end # # === EXAMPLE WITH COMPOSITE UNIQUE INDEX: # class Person < DataMapper::Base # property :server_id, :integer # property :name, :string # # index [:server_id, :name], :unique => true # end # # === SINGLE INDEX EXAMPLES: # * property :name, :index => true # * property :name, :index => :unique def index(indexes, unique = false) if indexes.kind_of?(Array) # if given an index of multiple columns database.schema[self].add_composite_index(indexes, unique) else raise ArgumentError.new("You must supply an array for the composite index") end end end # Lazy-loads the attributes for a loaded_set, then overwrites the # accessors # for the named methods so that the lazy_loading is skipped the second # time. def lazy_load!(*names) names = names.map { |name| name.to_sym }.reject { |name| lazy_loaded_attributes.include?(name) } reset_attribute = lambda do |instance| singleton_class = (class << instance; self end) names.each do |name| instance.lazy_loaded_attributes << name singleton_class.send(:attr_accessor, name) end end unless names.empty? || new_record? || loaded_set.nil? key = database_context.table(self.class).key.to_sym keys_to_select = loaded_set.map do |instance| instance.send(key) end database_context.all( self.class, :select => ([key] + names), :reload => true, key => keys_to_select ).each(&reset_attribute) else reset_attribute[self] end end def database_context @database_context || ( @database_context = database ) end def database_context=(value) @database_context = value end def logger self.class.logger end # Returns true if this model hasn't been saved to the # database, false otherwise. def new_record? @new_record.nil? || @new_record end # Returns a Set containing the properties that have had their # :lazy option set to true, or are lazily loaded by # default — i.e. text fields. def lazy_loaded_attributes @lazy_loaded_attributes || @lazy_loaded_attributes = Set.new end # Accepts a hash of properties and values to be updated and then calls #save def update_attributes(update_hash) self.attributes = update_hash self.save end # Returns true if the unsaved model has had properties changed # since it was loaded from the database. Returns false otherwise. def dirty?(cleared = Set.new) return false if cleared.include?(self) cleared << self result = database_context.table(self).columns.any? do |column| if column.type == :object Marshal.dump(self.instance_variable_get(column.instance_variable_name)) != original_values[column.name] else self.instance_variable_get(column.instance_variable_name) != original_values[column.name] end end return true if result loaded_associations.any? do |loaded_association| loaded_association.dirty?(cleared) end end # For unsaved models, returns a hash of properties that have had their # values changed since it was loaded from the database. def dirty_attributes pairs = {} database_context.table(self).columns.each do |column| value = instance_variable_get(column.instance_variable_name) if value != original_values[column.name] && (!new_record? || !column.serial?) pairs[column.name] = column.type != :object ? value : YAML.dump(value) end end pairs end def original_values=(values) values.each_pair do |k,v| original_values[k] = case v when String, Date, Time then v.dup # when column.type == :object then Marshal.dump(v) else v end end end def original_values class << self attr_reader :original_values end @original_values = {} end def loaded_set=(value) value << self @loaded_set = value end def inspect inspected_attributes = attributes.map { |k,v| "@#{k}=#{v.inspect}" } instance_variables.each do |name| if instance_variable_get(name).kind_of?(Associations::HasManyAssociation) inspected_attributes << "#{name}=#{instance_variable_get(name).inspect}" end end "#<%s:0x%x @new_record=%s, %s>" % [self.class.name, (object_id * 2), new_record?, inspected_attributes.join(', ')] end def loaded_associations @loaded_associations || @loaded_associations = [] end def key=(value) key_column = database_context.table(self.class).key @__key = key_column.type_cast_value(value) instance_variable_set(key_column.instance_variable_name, @__key) end def key @__key || @__key = begin key_column = database_context.table(self.class).key key_column.type_cast_value(instance_variable_get(key_column.instance_variable_name)) end end def keys self.class.table.keys.map do |column| column.type_cast_value(instance_variable_get(column.instance_variable_name)) end.compact end def <=>(other) keys <=> other.keys end # Look to ::included for __hash alias def hash @__hash || @__hash = keys.empty? ? super : keys.hash end def eql?(other) return false unless other.is_a?(self.class) || self.is_a?(other.class) comparator = keys.empty? ? :private_attributes : :keys send(comparator) == other.send(comparator) end def ==(other) eql?(other) end # Returns the difference between two objects, in terms of their # attributes. def ^(other) results = {} self_attributes, other_attributes = attributes, other.attributes self_attributes.each_pair do |k,v| other_value = other_attributes[k] unless v == other_value results[k] = [v, other_value] end end results end end end