Class: DataMapper::Property

Inherits:
Object
  • Object
show all
Extended by:
Deprecate, Equalizer
Includes:
Extlib::Assertions
Defined in:
lib/dm-core/property.rb,
lib/dm-core/adapters/oracle_adapter.rb

Overview

:include:QUICKLINKS

Properties

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.

Declaring Properties

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,  :nullable => false  # 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 types:

  • Boolean

  • String (default length is 50)

  • Text (limit of 65k characters by default)

  • Float

  • Integer

  • BigDecimal

  • DateTime

  • Date

  • Time

  • Object (marshalled out during serialization)

  • Class (datastore primitive is the same as String. Used for Inheritance)

Other types are known as custom types.

For more information about available Types, see DataMapper::Type

Limiting Access

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

Overriding Accessors

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 != 'Luke is Awesome'
    @title = new_title
  end
end

Lazy Loading

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::Types::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 multipule 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

Keys

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

Indices

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

Inferred Validations

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
    # => infers 'validates_length :title,
           :minimum => 0, :maximum => 250'

  property :title, String, :nullable => false
    # => infers 'validates_present :title

  property :email, String, :format => :email_address
    # => infers 'validates_format :email, :with => :email_address

  property :title, String, :length => 255, :nullable => false
    # => 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, part of the dm-more bundle. For more information about validations, check the documentation for dm-validations.

Default Values

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 => { |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.

Embedded Values

As an alternative to extraneous has_one relationships, consider using an EmbeddedValue.

Property options reference

: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

:nullable            if true, property may have a nil value on save

:key                 name of the key associated with this property.

:serial              if true, field value is auto incrementing

: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 type.

All other keys you pass to +property+ method are stored and available
as options[:extra_keys].

Misc. Notes

  • 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.

Constant Summary

OPTIONS =

NOTE: PLEASE update OPTIONS in DataMapper::Type when updating them here

[
  :accessor, :reader, :writer,
  :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
  :format, :index, :unique_index, :auto_validation,
  :validates, :unique, :precision, :scale, :min, :max
]
PRIMITIVES =
[
  TrueClass,
  String,
  Float,
  Integer,
  BigDecimal,
  DateTime,
  Date,
  Time,
  Object,
  Class,
].to_set.freeze
VISIBILITY_OPTIONS =

Possible :visibility option values

[ :public, :protected, :private ].to_set.freeze
DEFAULT_LENGTH =
50
DEFAULT_PRECISION =
10
DEFAULT_SCALE_BIGDECIMAL =

Default scale for BigDecimal type

0
DEFAULT_SCALE_FLOAT =

Default scale for Float type

nil
DEFAULT_NUMERIC_MIN =
0
DEFAULT_NUMERIC_MAX =
2**31-1

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Equalizer

equalize

Methods included from Deprecate

deprecate

Instance Attribute Details

#defaultObject (readonly)

Returns the value of attribute default



335
336
337
# File 'lib/dm-core/property.rb', line 335

def default
  @default
end

#instance_variable_nameObject (readonly)

Returns the value of attribute instance_variable_name



335
336
337
# File 'lib/dm-core/property.rb', line 335

def instance_variable_name
  @instance_variable_name
end

#maxObject (readonly)

Returns the value of attribute max



335
336
337
# File 'lib/dm-core/property.rb', line 335

def max
  @max
end

#minObject (readonly)

Returns the value of attribute min



335
336
337
# File 'lib/dm-core/property.rb', line 335

def min
  @min
end

#modelObject (readonly)

Returns the value of attribute model



335
336
337
# File 'lib/dm-core/property.rb', line 335

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name



335
336
337
# File 'lib/dm-core/property.rb', line 335

def name
  @name
end

#optionsObject (readonly)

Returns the value of attribute options



335
336
337
# File 'lib/dm-core/property.rb', line 335

def options
  @options
end

#precisionObject (readonly)

Returns the value of attribute precision



335
336
337
# File 'lib/dm-core/property.rb', line 335

def precision
  @precision
end

#primitiveObject (readonly)

Returns the value of attribute primitive



335
336
337
# File 'lib/dm-core/property.rb', line 335

def primitive
  @primitive
end

#reader_visibilityObject (readonly)

Returns the value of attribute reader_visibility



335
336
337
# File 'lib/dm-core/property.rb', line 335

def reader_visibility
  @reader_visibility
end

#repository_nameObject (readonly)

Returns the value of attribute repository_name



335
336
337
# File 'lib/dm-core/property.rb', line 335

def repository_name
  @repository_name
end

#scaleObject (readonly)

Returns the value of attribute scale



335
336
337
# File 'lib/dm-core/property.rb', line 335

def scale
  @scale
end

#typeObject (readonly)

Returns the value of attribute type



335
336
337
# File 'lib/dm-core/property.rb', line 335

def type
  @type
end

#writer_visibilityObject (readonly)

Returns the value of attribute writer_visibility



335
336
337
# File 'lib/dm-core/property.rb', line 335

def writer_visibility
  @writer_visibility
end

Instance Method Details

#custom?Boolean

Returns whether or not the property is custom (not provided by dm-core)



471
472
473
# File 'lib/dm-core/property.rb', line 471

def custom?
  @custom
end

#default?Boolean

Returns true if the property has a default value



684
685
686
# File 'lib/dm-core/property.rb', line 684

def default?
  @options.key?(:default)
end

#default_for(resource) ⇒ Object

Returns a default value of the property for given resource.

When default value is a callable object, it is called with resource and property passed as arguments.



670
671
672
673
674
675
676
# File 'lib/dm-core/property.rb', line 670

def default_for(resource)
  if @default.respond_to?(:call)
    @default.call(resource, self)
  else
    @default.try_dup
  end
end

#field(repository_name = nil) ⇒ String

Supplies the field in the data-store which the property corresponds to



344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/dm-core/property.rb', line 344

def field(repository_name = nil)
  if repository_name
    warn "Passing in +repository_name+ to #{self.class}#field is deprecated (#{caller[0]})"

    if repository_name != self.repository_name
      raise ArgumentError, "Mismatching +repository_name+ with #{self.class}#repository_name (#{repository_name.inspect} != #{self.repository_name.inspect})"
    end
  end

  # defer setting the field with the adapter specific naming
  # conventions until after the adapter has been setup
  @field ||= model.field_naming_convention(self.repository_name).call(self).freeze
end

#get(resource) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Standardized reader method for the property

Raises:

  • (ArgumentError)

    resource should be a Resource, but was .…”



486
487
488
489
490
491
492
493
494
# File 'lib/dm-core/property.rb', line 486

def get(resource)
  lazy_load(resource) unless loaded?(resource) || resource.new?

  if loaded?(resource)
    get!(resource)
  else
    set(resource, default? ? default_for(resource) : nil)
  end
end

#get!(resource) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Fetch the ivar value in the resource



505
506
507
# File 'lib/dm-core/property.rb', line 505

def get!(resource)
  resource.instance_variable_get(instance_variable_name)
end

#hashInteger

Returns the hash of the property name

This is necessary to allow comparisons between different properties in different models, having the same base model



378
379
380
# File 'lib/dm-core/property.rb', line 378

def hash
  name.hash
end

#indextrue, ...

Returns index name if property has index.



407
408
409
# File 'lib/dm-core/property.rb', line 407

def index
  @index
end

#inspectString

Returns a concise string representation of the property instance.



726
727
728
# File 'lib/dm-core/property.rb', line 726

def inspect
  "#<#{self.class.name} @model=#{model.inspect} @name=#{name.inspect}>"
end

#key?Boolean

Returns whether or not the property is a key or a part of a key



441
442
443
# File 'lib/dm-core/property.rb', line 441

def key?
  @key
end

#lazy?Boolean

Returns whether or not the property is to be lazy-loaded



431
432
433
# File 'lib/dm-core/property.rb', line 431

def lazy?
  @lazy
end

#lazy_load(resource) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Loads lazy columns when get or set is called.



595
596
597
# File 'lib/dm-core/property.rb', line 595

def lazy_load(resource)
  resource.send(:lazy_load, lazy_load_properties)
end

#lazy_load_propertiesObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

TODO: document



601
602
603
# File 'lib/dm-core/property.rb', line 601

def lazy_load_properties
  @lazy_load_properties ||= properties.in_context(lazy? ? [ self ] : properties.defaults)
end

#lengthInteger, NilClass

Returns maximum property length (if applicable). This usually only makes sense when property is of type Range or custom type.



390
391
392
393
394
395
396
# File 'lib/dm-core/property.rb', line 390

def length
  if @length.kind_of?(Range)
    @length.max
  else
    @length
  end
end

#loaded?(resource) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Check if the attribute corresponding to the property is loaded



585
586
587
# File 'lib/dm-core/property.rb', line 585

def loaded?(resource)
  resource.instance_variable_defined?(instance_variable_name)
end

#nullable?Boolean

Returns whether or not the property can accept 'nil' as it's value



461
462
463
# File 'lib/dm-core/property.rb', line 461

def nullable?
  @nullable
end

#primitive?(value) ⇒ Boolean

Test a value to see if it matches the primitive type



739
740
741
742
743
744
745
# File 'lib/dm-core/property.rb', line 739

def primitive?(value)
  if primitive == TrueClass
    value == true || value == false
  else
    value.kind_of?(primitive)
  end
end

#propertiesObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

TODO: document



607
608
609
# File 'lib/dm-core/property.rb', line 607

def properties
  @properties ||= model.properties(repository_name)
end

#serial?Boolean

Returns whether or not the property is “serial” (auto-incrementing)



451
452
453
# File 'lib/dm-core/property.rb', line 451

def serial?
  @serial
end

#set(resource, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Provides a standardized setter method for the property

Raises:

  • (ArgumentError)

    resource should be a Resource, but was .…”



547
548
549
550
551
552
553
554
555
556
557
558
559
# File 'lib/dm-core/property.rb', line 547

def set(resource, value)
  loaded   = loaded?(resource)
  original = get!(resource) if loaded
  value    = typecast(value)

  if loaded && value == original
    return original
  end

  set_original_value(resource, original)

  set!(resource, value)
end

#set!(resource, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Set the ivar value in the resource



572
573
574
# File 'lib/dm-core/property.rb', line 572

def set!(resource, value)
  resource.instance_variable_set(instance_variable_name, value)
end

#set_original_value(resource, original) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sets original value of the property on given resource. When property is set on DataMapper resource instance, original value is preserved. This makes possible to track dirty attributes and save only those really changed, and avoid extra queries to the data source in certain situations.



522
523
524
525
526
527
528
529
530
531
532
# File 'lib/dm-core/property.rb', line 522

def set_original_value(resource, original)
  original_attributes = resource.original_attributes
  original            = self.value(original)

  if original_attributes.key?(self)
    # stop tracking the value if it has not changed
    original_attributes.delete(self) if original == original_attributes[self] && resource.saved?
  else
    original_attributes[self] = original
  end
end

#typecast(value) ⇒ rue, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

typecasts values into a primitive (Ruby class that backs DataMapper property type). If property type can handle typecasting, it is delegated. How typecasting is perfomed, depends on the primitive of the type.

If type's primitive is a TrueClass, values of 1, t and true are casted to true.

For String primitive, to_s is called on value.

For Float primitive, to_f is called on value but only if value is a number otherwise value is returned.

For Integer primitive, to_i is called on value but only if value is a number, otherwise value is returned.

For BigDecimal primitive, to_d is called on value but only if value is a number, otherwise value is returned.

Casting to DateTime, Time and Date can handle both hashes with keys like :day or :hour and strings in format methods like Time.parse can handle.



638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/dm-core/property.rb', line 638

def typecast(value)
  return type.typecast(value, self) if type.respond_to?(:typecast)
  return value if primitive?(value) || value.nil?

  if    primitive == Integer    then typecast_to_integer(value)
  elsif primitive == String     then typecast_to_string(value)
  elsif primitive == TrueClass  then typecast_to_boolean(value)
  elsif primitive == BigDecimal then typecast_to_bigdecimal(value)
  elsif primitive == Float      then typecast_to_float(value)
  elsif primitive == DateTime   then typecast_to_datetime(value)
  elsif primitive == Time       then typecast_to_time(value)
  elsif primitive == Date       then typecast_to_date(value)
  elsif primitive == Class      then typecast_to_class(value)
  else
    value
  end
end

#unique?Boolean

Returns true if property is unique. Serial properties and keys are unique by default.



365
366
367
# File 'lib/dm-core/property.rb', line 365

def unique?
  @unique
end

#unique_indextrue, ...

Returns true if property has unique index. Serial properties and keys are unique by default.



421
422
423
# File 'lib/dm-core/property.rb', line 421

def unique_index
  @unique_index
end

#valid?(value) ⇒ Boolean

Test the value to see if it is a valid value for this Property



715
716
717
718
# File 'lib/dm-core/property.rb', line 715

def valid?(value)
  value = self.value(value)
  primitive?(value) || (value.nil? && nullable?)
end

#value(value) ⇒ Object

Returns given value unchanged for core types and uses dump method of the property type for custom types.



698
699
700
701
702
703
704
# File 'lib/dm-core/property.rb', line 698

def value(value)
  if custom?
    type.dump(value, self)
  else
    value
  end
end