Class: DataMapper::Property

Inherits:
Object
  • Object
show all
Includes:
Assertions
Defined in:
lib/dm-core/property.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 is a fundamental difference in thinking. 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.

  • 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, TrueClass, :default => false
     # Default value for new records is false
end

By default, DataMapper supports the following primitive types:

  • TrueClass, Boolean

  • String

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

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 accessors and mutators, 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 accessor for any property can be overridden in the same manner that Ruby class accessors can be. After the property is defined, just add your custom accessor:

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,   DataMapper::Types::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,     DataMapper::Types::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[1].components
  # loads when the post object is pulled from database, by default

Widget[1].components.first.body
  # loads the values for the body property on all objects in the
  # association, rather than just this one.

Widget[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[1]
   # when :id is the primary key on the users table
User['bill']
   # when :name is the primary (natural) key on the users table

Indeces

You can add indeces 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 indeces 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 indeces 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 => { |r, p| r.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.

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 collapse

PROPERTY_OPTIONS =

NOTE: check is only for psql, so maybe the postgres adapter should define its own property options. currently it will produce a warning tho since PROPERTY_OPTIONS is a constant

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

[
  :accessor, :reader, :writer,
  :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
  :format, :index, :unique_index, :check, :ordinal, :auto_validation,
  :validates, :unique, :track, :precision, :scale
]
TYPES =

FIXME: can we pull the keys from DataMapper::Adapters::DataObjectsAdapter::TYPES for this?

[
  TrueClass,
  String,
  DataMapper::Types::Text,
  Float,
  Integer,
  BigDecimal,
  DateTime,
  Date,
  Time,
  Object,
  Class,
  DataMapper::Types::Discriminator,
  DataMapper::Types::Serial
]
IMMUTABLE_TYPES =
[ TrueClass, Float, Integer, BigDecimal]
VISIBILITY_OPTIONS =
[ :public, :protected, :private ]
DEFAULT_LENGTH =
50
DEFAULT_PRECISION =
10
DEFAULT_SCALE_BIGDECIMAL =
0
DEFAULT_SCALE_FLOAT =
nil

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Assertions

#assert_kind_of

Instance Attribute Details

#defaultObject (readonly)

Returns the value of attribute default.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def default
  @default
end

#extra_optionsObject (readonly)

Returns the value of attribute extra_options.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def extra_options
  @extra_options
end

#getterObject (readonly)

Returns the value of attribute getter.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def getter
  @getter
end

#instance_variable_nameObject (readonly)

Returns the value of attribute instance_variable_name.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def instance_variable_name
  @instance_variable_name
end

#modelObject (readonly)

Returns the value of attribute model.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def model
  @model
end

#nameObject (readonly)

Returns the value of attribute name.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def name
  @name
end

#optionsObject (readonly)

Returns the value of attribute options.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def options
  @options
end

#precisionObject (readonly)

Returns the value of attribute precision.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def precision
  @precision
end

#primitiveObject (readonly)

Returns the value of attribute primitive.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def primitive
  @primitive
end

#reader_visibilityObject (readonly)

Returns the value of attribute reader_visibility.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def reader_visibility
  @reader_visibility
end

#scaleObject (readonly)

Returns the value of attribute scale.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def scale
  @scale
end

#trackObject (readonly)

Returns the value of attribute track.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def track
  @track
end

#typeObject (readonly)

Returns the value of attribute type.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def type
  @type
end

#writer_visibilityObject (readonly)

Returns the value of attribute writer_visibility.



295
296
297
# File 'lib/dm-core/property.rb', line 295

def writer_visibility
  @writer_visibility
end

Class Method Details

._load(marshalled) ⇒ 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.

TODO: add docs



523
524
525
526
# File 'lib/dm-core/property.rb', line 523

def self._load(marshalled)
  repository, model, name = Marshal.load(marshalled)
  model.properties(repository.name)[name]
end

Instance Method Details

#_dumpObject

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: add docs



517
518
519
# File 'lib/dm-core/property.rb', line 517

def _dump(*)
  Marshal.dump([ repository, model, name ])
end

#custom?Boolean

Returns:

  • (Boolean)


380
381
382
# File 'lib/dm-core/property.rb', line 380

def custom?
  @custom
end

#default_for(resource) ⇒ Object



503
504
505
# File 'lib/dm-core/property.rb', line 503

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

#eql?(o) ⇒ Boolean

Returns:

  • (Boolean)


321
322
323
324
325
326
327
# File 'lib/dm-core/property.rb', line 321

def eql?(o)
  if o.is_a?(Property)
    return o.model == @model && o.name == @name
  else
    return false
  end
end

#field(repository_name = nil) ⇒ String

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

-

Returns:

  • (String)

    name of field in data-store



304
305
306
# File 'lib/dm-core/property.rb', line 304

def field(repository_name = nil)
  @field || @fields[repository_name] ||= self.model.field_naming_convention(repository_name).call(self)
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.

Provides a standardized getter method for the property

-

Raises:

  • (ArgumentError)

    resource should be a DataMapper::Resource, but was .…”



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/dm-core/property.rb', line 389

def get(resource)
  lazy_load(resource)

  value = get!(resource)

  set_original_value(resource, value)

  # [YK] Why did we previously care whether options[:default] is nil.
  # The default value of nil will be applied either way
  if value.nil? && resource.new_record? && !resource.attribute_loaded?(name)
    value = default_for(resource)
    set(resource, value)
  end

  value
end

#get!(resource) ⇒ Object



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

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

#hashObject



312
313
314
315
316
317
318
319
# File 'lib/dm-core/property.rb', line 312

def hash
  if @custom && !@bound
    @type.bind(self)
    @bound = true
  end

  return @model.hash + @name.hash
end

#indexObject



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

def index
  @index
end

#inspectObject



511
512
513
# File 'lib/dm-core/property.rb', line 511

def inspect
  "#<Property:#{@model}:#{@name}>"
end

#key?TrueClass, FalseClass

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

-

Returns:

  • (TrueClass, FalseClass)

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



358
359
360
# File 'lib/dm-core/property.rb', line 358

def key?
  @key
end

#lazy?TrueClass, FalseClass

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

-

Returns:

  • (TrueClass, FalseClass)

    whether or not the property is to be lazy-loaded



348
349
350
# File 'lib/dm-core/property.rb', line 348

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



447
448
449
450
451
452
453
454
455
456
# File 'lib/dm-core/property.rb', line 447

def lazy_load(resource)
  # It is faster to bail out at at a new_record? rather than to process
  # which properties would be loaded and then not load them.
  return if resource.new_record? || resource.attribute_loaded?(name)
  # If we're trying to load a lazy property, load it. Otherwise, lazy-load
  # any properties that should be eager-loaded but were not included
  # in the original :fields list
  contexts = lazy? ? name : model.eager_properties(resource.repository.name)
  resource.send(:lazy_load, contexts)
end

#lengthObject Also known as: size



329
330
331
# File 'lib/dm-core/property.rb', line 329

def length
  @length.is_a?(Range) ? @length.max : @length
end

#nullable?TrueClass, FalseClass

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

-

Returns:

  • (TrueClass, FalseClass)

    whether or not the property can accept ‘nil’



376
377
378
# File 'lib/dm-core/property.rb', line 376

def nullable?
  @nullable
end

#serial?TrueClass, FalseClass

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

-

Returns:

  • (TrueClass, FalseClass)

    whether or not the property is “serial”



367
368
369
# File 'lib/dm-core/property.rb', line 367

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 DataMapper::Resource, but was .…”



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/dm-core/property.rb', line 423

def set(resource, value)
  # [YK] We previously checked for new_record? here, but lazy loading
  # is blocked anyway if we're in a new record by by
  # Resource#reload_attributes. This may eventually be useful for
  # optimizing, but let's (a) benchmark it first, and (b) do
  # whatever refactoring is necessary, which will benefit from the
  # centralize checking
  lazy_load(resource)

  new_value = typecast(value)
  old_value = get!(resource)

  set_original_value(resource, old_value)

  set!(resource, new_value)
end

#set!(resource, value) ⇒ Object



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

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

#set_original_value(resource, val) ⇒ Object



410
411
412
413
414
415
416
# File 'lib/dm-core/property.rb', line 410

def set_original_value(resource, val)
  unless resource.original_values.key?(name)
    val = val.try_dup
    val = val.hash if track == :hash
    resource.original_values[name] = val
  end
end

#typecast(value) ⇒ TrueClass, ...

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

-

Returns:

  • (TrueClass, String, Float, Integer, BigDecimal, DateTime, Date, Time Class)

    the primitive data-type, defaults to TrueClass



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'lib/dm-core/property.rb', line 464

def typecast(value)
  return type.typecast(value, self) if type.respond_to?(:typecast)
  return value if value.kind_of?(primitive) || value.nil?
  begin
    if    primitive == TrueClass  then %w[ true 1 t ].include?(value.to_s.downcase)
    elsif primitive == String     then value.to_s
    elsif primitive == Float      then value.to_f
    elsif primitive == Integer
      # The simplest possible implementation, i.e. value.to_i, is not
      # desirable because "junk".to_i gives "0". We want nil instead,
      # because this makes it clear that the typecast failed.
      #
      # After benchmarking, we preferred the current implementation over
      # these two alternatives:
      # * Integer(value) rescue nil
      # * Integer(value_to_s =~ /(\d+)/ ? $1 : value_to_s) rescue nil
      #
      # [YK] The previous implementation used a rescue. Why use a rescue
      # when the list of cases where a valid string other than "0" could
      # produce 0 is known?
      value_to_i = value.to_i
      if value_to_i == 0
        value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
      else
        value_to_i
      end
    elsif primitive == BigDecimal then BigDecimal(value.to_s)
    elsif primitive == DateTime   then typecast_to_datetime(value)
    elsif primitive == Date       then typecast_to_date(value)
    elsif primitive == Time       then typecast_to_time(value)
    elsif primitive == Class      then self.class.find_const(value)
    else
      value
    end
  rescue
    value
  end
end

#uniqueObject



308
309
310
# File 'lib/dm-core/property.rb', line 308

def unique
  @unique ||= @options.fetch(:unique, @serial || @key || false)
end

#unique_indexObject



338
339
340
# File 'lib/dm-core/property.rb', line 338

def unique_index
  @unique_index
end

#value(val) ⇒ Object



507
508
509
# File 'lib/dm-core/property.rb', line 507

def value(val)
  custom? ? self.type.dump(val, self) : val
end