Properties for a model are not derived from a database structure, but instead explicitly declared inside your model class definitions. These properties then map (or, if using automigrate, generate) fields in your repository/database.
If you are coming to DataMapper from another ORM framework, such as ActiveRecord, this may be a fundamental difference in thinking to you. However, there are several advantages to defining your properties in your models:
information about your model is centralized in one place: rather than having to dig out migrations, xml or other configuration files.
use of mixins can be applied to model properties: better code reuse
having information centralized in your models, encourages you and the developers on your team to take a model-centric view of development.
it provides the ability to use Ruby’s access control functions.
and, because DataMapper only cares about properties explicitly defined in your models, DataMapper plays well with legacy databases, and shares databases easily with other applications.
Inside your class, you call the property method for each property you want to add. The only two required arguments are the name and type, everything else is optional.
class Post include DataMapper::Resource property :title, String, :required => true # Cannot be null property :publish, Boolean, :default => false # Default value for new records is false end
By default, DataMapper supports the following primitive (Ruby) types also called core properties:
Class (datastore primitive is the same as String. Used for Inheritance)
Object (marshalled out during serialization)
String (default length is 50)
Text (limit of 65k characters by default)
Property access control is uses the same terminology Ruby does. Properties are public by default, but can also be declared private or protected as needed (via the :accessor option).
class Post include DataMapper::Resource property :title, String, :accessor => :private # Both reader and writer are private property :body, Text, :accessor => :protected # Both reader and writer are protected end
Access control is also analogous to Ruby attribute readers and writers, and can be declared using :reader and :writer, in addition to :accessor.
class Post include DataMapper::Resource property :title, String, :writer => :private # Only writer is private property :tags, String, :reader => :protected # Only reader is protected end
The reader/writer for any property can be overridden in the same manner that Ruby attr readers/writers can be. After the property is defined, just add your custom reader or writer:
class Post include DataMapper::Resource property :title, String def title=(new_title) raise ArgumentError if new_title != 'Lee is l337' super(new_title) end end
Calling super ensures that any validators defined for the property are kept active.
By default, some properties are not loaded when an object is fetched in DataMapper. These lazily loaded properties are fetched on demand when their accessor is called for the first time (as it is often unnecessary to instantiate -every- property -every- time an object is loaded). For instance, DataMapper::Property::Text fields are lazy loading by default, although you can over-ride this behavior if you wish:
Example:
class Post include DataMapper::Resource property :title, String # Loads normally property :body, Text # Is lazily loaded by default end
If you want to over-ride the lazy loading on any field you can set it to a context or false to disable it with the :lazy option. Contexts allow multiple lazy properties to be loaded at one time. If you set :lazy to true, it is placed in the :default context
class Post include DataMapper::Resource property :title, String # Loads normally property :body, Text, :lazy => false # The default is now over-ridden property :comment, String, :lazy => [ :detailed ] # Loads in the :detailed context property :author, String, :lazy => [ :summary, :detailed ] # Loads in :summary & :detailed context end
Delaying the request for lazy-loaded attributes even applies to objects accessed through associations. In a sense, DataMapper anticipates that you will likely be iterating over objects in associations and rolls all of the load commands for lazy-loaded properties into one request from the database.
Example:
Widget.get(1).components # loads when the post object is pulled from database, by default Widget.get(1).components.first.body # loads the values for the body property on all objects in the # association, rather than just this one. Widget.get(1).components.first.comment # loads both comment and author for all objects in the association # since they are both in the :detailed context
Properties can be declared as primary or natural keys on a table. You should a property as the primary key of the table:
Examples:
property :id, Serial # auto-incrementing key property :legacy_pk, String, :key => true # 'natural' key
This is roughly equivalent to ActiveRecord’s set_primary_key, though non-integer data types may be used, thus DataMapper supports natural keys. When a property is declared as a natural key, accessing the object using the indexer syntax Class[key] remains valid.
User.get(1) # when :id is the primary key on the users table User.get('bill') # when :name is the primary (natural) key on the users table
You can add indices for your properties by using the :index option. If you use true as the option value, the index will be automatically named. If you want to name the index yourself, use a symbol as the value.
property :last_name, String, :index => true property :first_name, String, :index => :name
You can create multi-column composite indices by using the same symbol in all the columns belonging to the index. The columns will appear in the index in the order they are declared.
property :last_name, String, :index => :name property :first_name, String, :index => :name # => index on (last_name, first_name)
If you want to make the indices unique, use :unique_index instead of :index
If you require the dm-validations plugin, auto-validations will automatically be mixed-in in to your model classes: validation rules that are inferred when properties are declared with specific column restrictions.
class Post include DataMapper::Resource property :title, String, :length => 250, :min => 0, :max => 250 # => infers 'validates_length :title' property :title, String, :required => true # => infers 'validates_present :title' property :email, String, :format => :email_address # => infers 'validates_format :email, :with => :email_address' property :title, String, :length => 255, :required => true # => infers both 'validates_length' as well as 'validates_present' # better: property :title, String, :length => 1..255 end
This functionality is available with the dm-validations gem. For more information about validations, check the documentation for dm-validations.
To set a default for a property, use the :default key. The property will be set to the value associated with that key the first time it is accessed, or when the resource is saved if it hasn’t been set with another value already. This value can be a static value, such as ‘hello’ but it can also be a proc that will be evaluated when the property is read before its value has been set. The property is set to the return of the proc. The proc is passed two values, the resource the property is being set for and the property itself.
property :display_name, String, :default => lambda { |resource, property| resource.login }
Word of warning. Don’t try to read the value of the property you’re setting the default for in the proc. An infinite loop will ensue.
As an alternative to extraneous has_one relationships, consider using an EmbeddedValue.
:accessor if false, neither reader nor writer methods are created for this property :reader if false, reader method is not created for this property :writer if false, writer method is not created for this property :lazy if true, property value is only loaded when on first read if false, property value is always loaded if a symbol, property value is loaded with other properties in the same group :default default value of this property :allow_nil if true, property may have a nil value on save :key name of the key associated with this property. :field field in the data-store which the property corresponds to :length string field length :format format for autovalidation. Use with dm-validations plugin. :index if true, index is created for the property. If a Symbol, index is named after Symbol value instead of being based on property name. :unique_index true specifies that index on this property should be unique :auto_validation if true, automatic validation is performed on the property :validates validation context. Use together with dm-validations. :unique if true, property column is unique. Properties of type Serial are unique by default. :precision Indicates the number of significant digits. Usually only makes sense for float type properties. Must be >= scale option value. Default is 10. :scale The number of significant digits to the right of the decimal point. Only makes sense for float type properties. Must be > 0. Default is nil for Float type and 10 for BigDecimal
There is the ability to reconfigure a Property and it’s subclasses by explicitly setting a value in the Property, eg:
# set all String properties to have a default length of 255 DataMapper::Property::String.length(255) # set all Boolean properties to not allow nil (force true or false) DataMapper::Property::Boolean.allow_nil(false) # set all properties to be required by default DataMapper::Property.required(true) # turn off auto-validation for all properties by default DataMapper::Property.auto_validation(false) # set all mutator methods to be private by default DataMapper::Property.writer(:private)
Please note that this has no effect when a subclass has explicitly defined it’s own option. For example, setting the String length to 255 will not affect the Text property even though it inherits from String, because it sets it’s own default length to 65535.
Properties declared as strings will default to a length of 50, rather than 255 (typical max varchar column size). To overload the default, pass :length => 255 or :length => 0..255. Since DataMapper does not introspect for properties, this means that legacy database tables may need their String columns defined with a :length so that DM does not apply an un-needed length validation, or allow overflow.
You may declare a Property with the data-type of Class. see SingleTableInheritance for more on how to use Class columns.
Possible :visibility option values
Invalid property names
Returns index name if property has index.
@return [Boolean, Symbol, Array]
returns true if property is indexed by itself returns a Symbol if the property is indexed with other properties returns an Array if the property belongs to multiple indexes returns false if the property does not belong to any indexes
@api public
Returns true if property has unique index. Serial properties and keys are unique by default.
@return [Boolean, Symbol, Array]
returns true if property is indexed by itself returns a Symbol if the property is indexed with other properties returns an Array if the property belongs to multiple indexes returns false if the property does not belong to any indexes
@api public
@api public
# File lib/dm-core/property.rb, line 424 424: def accept_options(*args) 425: accepted_options.concat(args) 426: 427: # create methods for each new option 428: args.each do |property_option| 429: class_eval def self.#{property_option}(value = Undefined) # def self.unique(value = Undefined) return @#{property_option} if value.equal?(Undefined) # return @unique if value.equal?(Undefined) descendants.each do |descendant| # descendants.each do |descendant| unless descendant.instance_variable_defined?(:@#{property_option}) # unless descendant.instance_variable_defined?(:@unique) descendant.#{property_option}(value) # descendant.unique(value) end # end end # end @#{property_option} = value # @unique = value end # end, __FILE__, __LINE__ + 1 430: end 431: 432: descendants.each { |descendant| descendant.accepted_options.concat(args) } 433: end
@api public
# File lib/dm-core/property.rb, line 419 419: def accepted_options 420: @accepted_options ||= [] 421: end
@api private
# File lib/dm-core/property.rb, line 371 371: def demodulized_names 372: @demodulized_names ||= {} 373: end
@api public
# File lib/dm-core/property.rb, line 383 383: def descendants 384: @descendants ||= DescendantSet.new 385: end
@api semipublic
# File lib/dm-core/property.rb, line 365 365: def determine_class(type) 366: return type if type < DataMapper::Property::Object 367: find_class(DataMapper::Inflector.demodulize(type.name)) 368: end
@api semipublic
# File lib/dm-core/property.rb, line 376 376: def find_class(name) 377: klass = demodulized_names[name] 378: klass ||= const_get(name) if const_defined?(name) 379: klass 380: end
@api private
# File lib/dm-core/property.rb, line 388 388: def inherited(descendant) 389: # Descendants is a tree rooted in DataMapper::Property that tracks 390: # inheritance. We pre-calculate each comparison value (demodulized 391: # class name) to achieve a Hash[]-time lookup, rather than walk the 392: # entire descendant tree and calculate names on-demand (expensive, 393: # redundant). 394: # 395: # Since the algorithm relegates property class name lookups to a flat 396: # namespace, we need to ensure properties defined outside of DM don't 397: # override built-ins (Serial, String, etc) by merely defining a property 398: # of a same name. We avoid this by only ever adding to the lookup 399: # table. Given that DM loads its own property classes first, we can 400: # assume that their names are "reserved" when added to the table. 401: # 402: # External property authors who want to provide "replacements" for 403: # builtins (e.g. in a non-DM-supported adapter) should follow the 404: # convention of wrapping those properties in a module, and include'ing 405: # the module on the model class directly. This bypasses the DM-hooked 406: # const_missing lookup that would normally check this table. 407: descendants << descendant 408: 409: Property.demodulized_names[DataMapper::Inflector.demodulize(descendant.name)] ||= descendant 410: 411: # inherit accepted options 412: descendant.accepted_options.concat(accepted_options) 413: 414: # inherit the option values 415: options.each { |key, value| descendant.send(key, value) } 416: end
@api semipublic
# File lib/dm-core/property.rb, line 735 735: def initialize(model, name, options = {}) 736: options = options.to_hash.dup 737: 738: if INVALID_NAMES.include?(name.to_s) || (kind_of?(Boolean) && INVALID_NAMES.include?("#{name}?")) 739: raise ArgumentError, 740: "+name+ was #{name.inspect}, which cannot be used as a property name since it collides with an existing method or a query option" 741: end 742: 743: assert_valid_options(options) 744: 745: predefined_options = self.class.options 746: 747: @repository_name = model.repository_name 748: @model = model 749: @name = name.to_s.chomp('?').to_sym 750: @options = predefined_options.merge(options).freeze 751: @instance_variable_name = "@#{@name}".freeze 752: 753: @primitive = self.class.primitive 754: @field = @options[:field].freeze unless @options[:field].nil? 755: @default = @options[:default] 756: 757: @serial = @options.fetch(:serial, false) 758: @key = @options.fetch(:key, @serial) 759: @unique = @options.fetch(:unique, @key ? :key : false) 760: @required = @options.fetch(:required, @key) 761: @allow_nil = @options.fetch(:allow_nil, !@required) 762: @allow_blank = @options.fetch(:allow_blank, !@required) 763: @index = @options.fetch(:index, false) 764: @unique_index = @options.fetch(:unique_index, @unique) 765: @lazy = @options.fetch(:lazy, false) && !@key 766: 767: determine_visibility 768: 769: bind 770: end
@api private
# File lib/dm-core/property.rb, line 446 446: def nullable(*args) 447: # :required is preferable to :allow_nil, but :nullable maps precisely to :allow_nil 448: raise "#nullable is deprecated, use #required instead (#{caller.first})" 449: end
Gives all the options set on this property
@return [Hash] with all options and their values set on this property
@api public
# File lib/dm-core/property.rb, line 456 456: def options 457: options = {} 458: accepted_options.each do |name| 459: options[name] = send(name) if instance_variable_defined?("@#{name}") 460: end 461: options 462: end
Returns whether or not the property can be a blank value
@return [Boolean]
whether or not the property can be blank
@api public
# File lib/dm-core/property.rb, line 579 579: def allow_blank? 580: @allow_blank 581: end
Returns whether or not the property can accept ‘nil’ as it’s value
@return [Boolean]
whether or not the property can accept 'nil'
@api public
# File lib/dm-core/property.rb, line 569 569: def allow_nil? 570: @allow_nil 571: end
A hook to allow properties to extend or modify the model it’s bound to. Implementations are not supposed to modify the state of the property class, and should produce no side-effects on the property instance.
# File lib/dm-core/property.rb, line 470 470: def bind 471: # no op 472: end
Supplies the field in the data-store which the property corresponds to
@return [String] name of field in data-store
@api semipublic
# File lib/dm-core/property.rb, line 479 479: def field(repository_name = nil) 480: if repository_name 481: raise "Passing in +repository_name+ to #{self.class}#field is deprecated (#{caller.first})" 482: end 483: 484: # defer setting the field with the adapter specific naming 485: # conventions until after the adapter has been setup 486: @field ||= model.field_naming_convention(self.repository_name).call(self).freeze 487: end
Standardized reader method for the property
@param [Resource] resource
model instance for which this property is to be loaded
@return [Object]
the value of this property for the provided instance
@raise [ArgumentError] “resource should be a Resource, but was .…“
@api private
# File lib/dm-core/property.rb, line 594 594: def get(resource) 595: get!(resource) 596: end
Fetch the ivar value in the resource
@param [Resource] resource
model instance for which this property is to be unsafely loaded
@return [Object]
current @ivar value of this property in +resource+
@api private
# File lib/dm-core/property.rb, line 607 607: def get!(resource) 608: resource.instance_variable_get(instance_variable_name) 609: end
Returns a concise string representation of the property instance.
@return [String]
Concise string representation of the property instance.
@api public
# File lib/dm-core/property.rb, line 715 715: def inspect 716: "#<#{self.class.name} @model=#{model.inspect} @name=#{name.inspect}>" 717: end
Returns whether or not the property is a key or a part of a key
@return [Boolean]
true if the property is a key or a part of a key
@api public
# File lib/dm-core/property.rb, line 539 539: def key? 540: @key 541: end
Returns whether or not the property is to be lazy-loaded
@return [Boolean]
true if the property is to be lazy-loaded
@api public
# File lib/dm-core/property.rb, line 529 529: def lazy? 530: @lazy 531: end
Loads lazy columns when get or set is called.
@param [Resource] resource
model instance for which lazy loaded attribute are loaded
@api private
# File lib/dm-core/property.rb, line 662 662: def lazy_load(resource) 663: return if loaded?(resource) 664: resource.__send__(:lazy_load, lazy_load_properties) 665: end
@api private
# File lib/dm-core/property.rb, line 668 668: def lazy_load_properties 669: @lazy_load_properties ||= 670: begin 671: properties = self.properties 672: properties.in_context(lazy? ? [ self ] : properties.defaults) 673: end 674: end
Check if the attribute corresponding to the property is loaded
@param [Resource] resource
model instance for which the attribute is to be tested
@return [Boolean]
true if the attribute is loaded in the resource
@api private
# File lib/dm-core/property.rb, line 652 652: def loaded?(resource) 653: resource.instance_variable_defined?(instance_variable_name) 654: end
Test a value to see if it matches the primitive type
@param [Object] value
value to test
@return [Boolean]
true if the value is the correct type
@api semipublic
# File lib/dm-core/property.rb, line 728 728: def primitive?(value) 729: value.kind_of?(primitive) 730: end
@api private
# File lib/dm-core/property.rb, line 677 677: def properties 678: @properties ||= model.properties(repository_name) 679: end
Returns whether or not the property must be non-nil and non-blank
@return [Boolean]
whether or not the property is required
@api public
# File lib/dm-core/property.rb, line 559 559: def required? 560: @required 561: end
Returns whether or not the property is “serial” (auto-incrementing)
@return [Boolean]
whether or not the property is "serial"
@api public
# File lib/dm-core/property.rb, line 549 549: def serial? 550: @serial 551: end
Provides a standardized setter method for the property
@param [Resource] resource
the resource to get the value from
@param [Object] value
the value to set in the resource
@return [Object]
+value+ after being typecasted according to this property's primitive
@raise [ArgumentError] “resource should be a Resource, but was .…“
@api private
# File lib/dm-core/property.rb, line 624 624: def set(resource, value) 625: set!(resource, typecast(value)) 626: end
Set the ivar value in the resource
@param [Resource] resource
the resource to set
@param [Object] value
the value to set in the resource
@return [Object]
the value set in the resource
@api private
# File lib/dm-core/property.rb, line 639 639: def set!(resource, value) 640: resource.instance_variable_set(instance_variable_name, value) 641: end
@api semipublic
# File lib/dm-core/property.rb, line 682 682: def typecast(value) 683: if value.nil? || primitive?(value) 684: value 685: elsif respond_to?(:typecast_to_primitive) 686: typecast_to_primitive(value) 687: end 688: end
Returns true if property is unique. Serial properties and keys are unique by default.
@return [Boolean]
true if property has uniq index defined, false otherwise
@api public
# File lib/dm-core/property.rb, line 496 496: def unique? 497: !!@unique 498: end
Test the value to see if it is a valid value for this Property
@param [Object] loaded_value
the value to be tested
@return [Boolean]
true if the value is valid
@api semipulic
# File lib/dm-core/property.rb, line 699 699: def valid?(value, negated = false) 700: dumped_value = dump(value) 701: 702: if required? && dumped_value.nil? 703: negated || false 704: else 705: primitive?(dumped_value) || (dumped_value.nil? && (allow_nil? || negated)) 706: end 707: end
@api private
# File lib/dm-core/property.rb, line 773 773: def assert_valid_options(options) 774: keys = options.keys 775: 776: if (unknown_keys = keys - self.class.accepted_options).any? 777: raise ArgumentError, "options #{unknown_keys.map { |key| key.inspect }.join(' and ')} are unknown" 778: end 779: 780: options.each do |key, value| 781: boolean_value = value == true || value == false 782: 783: case key 784: when :field 785: assert_kind_of "options[:#{key}]", value, ::String 786: 787: when :default 788: if value.nil? 789: raise ArgumentError, "options[:#{key}] must not be nil" 790: end 791: 792: when :serial, :key, :allow_nil, :allow_blank, :required, :auto_validation 793: unless boolean_value 794: raise ArgumentError, "options[:#{key}] must be either true or false" 795: end 796: 797: if key == :required && (keys.include?(:allow_nil) || keys.include?(:allow_blank)) 798: raise ArgumentError, 'options[:required] cannot be mixed with :allow_nil or :allow_blank' 799: end 800: 801: when :index, :unique_index, :unique, :lazy 802: unless boolean_value || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.any? && value.all? { |val| val.kind_of?(Symbol) }) 803: raise ArgumentError, "options[:#{key}] must be either true, false, a Symbol or an Array of Symbols" 804: end 805: 806: when :length 807: assert_kind_of "options[:#{key}]", value, Range, ::Integer 808: 809: when :size, :precision, :scale 810: assert_kind_of "options[:#{key}]", value, ::Integer 811: 812: when :reader, :writer, :accessor 813: assert_kind_of "options[:#{key}]", value, Symbol 814: 815: unless VISIBILITY_OPTIONS.include?(value) 816: raise ArgumentError, "options[:#{key}] must be #{VISIBILITY_OPTIONS.join(' or ')}" 817: end 818: end 819: end 820: end
Assert given visibility value is supported.
Will raise ArgumentError if this Property’s reader and writer visibilities are not included in VISIBILITY_OPTIONS.
@return [undefined]
@raise [ArgumentError] “property visibility must be :public, :protected, or :private“
@api private
# File lib/dm-core/property.rb, line 832 832: def determine_visibility 833: default_accessor = @options.fetch(:accessor, :public) 834: 835: @reader_visibility = @options.fetch(:reader, default_accessor) 836: @writer_visibility = @options.fetch(:writer, default_accessor) 837: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.