Class | DataMapper::Property |
In: |
lib/data_mapper/property.rb
lib/data_mapper/property.rb |
Parent: | Object |
A model‘s properties are not derived from database structure. Instead, properties are declared inside it‘s model‘s class definition, which map to (or generate) fields in a database.
Defining properties explicitly in a model has several advantages. It centralizes information about the model in a single location, rather than having to dig out migrations, xml, or other config files. It also provides the ability to use Ruby‘s access control functions. Finally, since Datamapper only cares about properties explicitly defined in your models, Datamappers 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 < DataMapper::Base property :title, :string, :nullable => false # Cannot be null property :publish, :boolen, :default => false # Default value for new records is false end
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 < DataMapper::Base 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 getters, setters, and accessors, and can be declared using :reader and :writer, in addition to :accessor.
class Post < DataMapper::Base property :title, :string, :writer => :private # Only writer is private property :tags, :string, :reader => :protected # Only reader is protected end
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 < DataMapper::Base property :title, :string def title=(new_title) raise ArgumentError if new_title != 'Luke is Awesome' @title = new_title end end
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, text fields are lazy loading by default, although you can over-ride this behavior if you wish:
Example:
class Post < DataMapper::Base 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 true or false with the :lazy option.
class Post < DataMapper::Base property :title, :string # Loads normally property :body, :text, :lazy => false # The default is now over-ridden 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.
Properties can be declared as primary or natural keys on a table. By default, Datamapper will assume :id and create it if you don‘t have it. You can, however, declare a property as the primary key of the table:
property :legacy_pk, :string, :key => true
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
When properties are declared with specific column restrictions, Datamapper will infer a few validation rules for values assigned to that property.
property :title, :string, :length => 250 # => infers 'validates_length_of :title, :minimum => 0, :maximum => 250' property :title, :string, :nullable => false # => infers 'validates_presence_of :title property :email, :string, :format => :email_address # => infers 'validates_format_of :email, :with => :email_address property :title, :string, :length => 255, :nullable => false # => infers both 'validates_length_of' as well as 'validates_presence_of' # better: property :title, :string, :length => 1..255
For more information about validations, visit the Validatable documentation.
As an alternative to extraneous has_one relationships, consider using an EmbeddedValue.
PROPERTY_OPTIONS | = | [ :public, :protected, :private, :accessor, :reader, :writer, :lazy, :default, :nullable, :key, :serial, :column, :size, :length, :format, :index, :check, :ordinal, :auto_validation | 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 | |
VISIBILITY_OPTIONS | = | [:public, :protected, :private] | ||
AUTO_VALIDATIONS | = | { :nullable => lambda { |k,v| "validates_presence_of :#{k}" if v == false }, :size => lambda { |k,v| "validates_length_of :#{k}, " + (v.is_a?(Range) ? ":within => #{v}" : ":maximum => #{v}") }, :format => lambda { |k, v| "validates_format_of :#{k}, :with => #{v.inspect}" } | NOTE: :length may also be used in place of :size | |
PROPERTY_OPTIONS | = | [ :public, :protected, :private, :accessor, :reader, :writer, :lazy, :default, :nullable, :key, :serial, :column, :size, :length, :format, :index, :check, :ordinal, :auto_validation | 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 | |
VISIBILITY_OPTIONS | = | [:public, :protected, :private] | ||
AUTO_VALIDATIONS | = | { :nullable => lambda { |k,v| "validates_presence_of :#{k}" if v == false }, :size => lambda { |k,v| "validates_length_of :#{k}, " + (v.is_a?(Range) ? ":within => #{v}" : ":maximum => #{v}") }, :format => lambda { |k, v| "validates_format_of :#{k}, :with => #{v.inspect}" } | NOTE: :length may also be used in place of :size |
# File lib/data_mapper/property.rb, line 153 153: def initialize(klass, name, type, options) 154: 155: @klass, @name, @type, @options = klass, name.to_sym, type, options 156: @symbolized_name = name.to_s.sub(/\?$/, '').to_sym 157: 158: validate_type! 159: validate_options! 160: determine_visibility! 161: 162: database.schema[klass].add_column(@symbolized_name, @type, @options) 163: klass::ATTRIBUTES << @symbolized_name 164: 165: create_getter! 166: create_setter! 167: auto_validations! unless @options[:auto_validation] == false 168: 169: end
# File lib/data_mapper/property.rb, line 153 153: def initialize(klass, name, type, options) 154: 155: @klass, @name, @type, @options = klass, name.to_sym, type, options 156: @symbolized_name = name.to_s.sub(/\?$/, '').to_sym 157: 158: validate_type! 159: validate_options! 160: determine_visibility! 161: 162: database.schema[klass].add_column(@symbolized_name, @type, @options) 163: klass::ATTRIBUTES << @symbolized_name 164: 165: create_getter! 166: create_setter! 167: auto_validations! unless @options[:auto_validation] == false 168: 169: end
defines the inferred validations given a property definition.
# File lib/data_mapper/property.rb, line 262 262: def auto_validations! 263: AUTO_VALIDATIONS.each do |key, value| 264: next unless options.has_key?(key) 265: validation = value.call(name, options[key]) 266: next if validation.nil? or validation.empty? 267: klass.class_eval "begin\n\#{validation}\nrescue ArgumentError => e\nthrow e unless e.message =~ /specify a unique key/\nend\n" 268: end 269: end
defines the inferred validations given a property definition.
# File lib/data_mapper/property.rb, line 262 262: def auto_validations! 263: AUTO_VALIDATIONS.each do |key, value| 264: next unless options.has_key?(key) 265: validation = value.call(name, options[key]) 266: next if validation.nil? or validation.empty? 267: klass.class_eval "begin\n\#{validation}\nrescue ArgumentError => e\nthrow e unless e.message =~ /specify a unique key/\nend\n" 268: end 269: end
# File lib/data_mapper/property.rb, line 282 282: def column 283: column = database.table(klass)[@name] 284: raise StandardError.new("#{@name.inspect} is not a valid column name") unless column 285: return column 286: end
# File lib/data_mapper/property.rb, line 282 282: def column 283: column = database.table(klass)[@name] 284: raise StandardError.new("#{@name.inspect} is not a valid column name") unless column 285: return column 286: end
defines the getter for the property
# File lib/data_mapper/property.rb, line 191 191: def create_getter! 192: if lazy? 193: klass.class_eval "\#{reader_visibility.to_s}\ndef \#{name}\nlazy_load!(\#{name.inspect})\nclass << self;\nattr_accessor \#{name.inspect}\nend\n@\#{name}\nend\n" 194: else 195: klass.class_eval "\#{reader_visibility.to_s}\ndef \#{name}\n\#{instance_variable_name}\nend\n" 196: end 197: if type == :boolean 198: klass.class_eval "\#{reader_visibility.to_s}\ndef \#{name.to_s.ensure_ends_with('?')}\n\#{instance_variable_name}\nend\n" 199: end 200: rescue SyntaxError 201: raise SyntaxError.new(column) 202: end
defines the getter for the property
# File lib/data_mapper/property.rb, line 191 191: def create_getter! 192: if lazy? 193: klass.class_eval "\#{reader_visibility.to_s}\ndef \#{name}\nlazy_load!(\#{name.inspect})\nclass << self;\nattr_accessor \#{name.inspect}\nend\n@\#{name}\nend\n" 194: else 195: klass.class_eval "\#{reader_visibility.to_s}\ndef \#{name}\n\#{instance_variable_name}\nend\n" 196: end 197: if type == :boolean 198: klass.class_eval "\#{reader_visibility.to_s}\ndef \#{name.to_s.ensure_ends_with('?')}\n\#{instance_variable_name}\nend\n" 199: end 200: rescue SyntaxError 201: raise SyntaxError.new(column) 202: end
defines the setter for the property
# File lib/data_mapper/property.rb, line 227 227: def create_setter! 228: if lazy? 229: klass.class_eval "\#{writer_visibility.to_s}\ndef \#{name}=(value)\nclass << self;\nattr_accessor \#{name.inspect}\nend\n@\#{name} = value\nend\n" 230: else 231: klass.class_eval "\#{writer_visibility.to_s}\ndef \#{name}=(value)\n\#{instance_variable_name} = value\nend\n" 232: end 233: rescue SyntaxError 234: raise SyntaxError.new(column) 235: end
defines the setter for the property
# File lib/data_mapper/property.rb, line 227 227: def create_setter! 228: if lazy? 229: klass.class_eval "\#{writer_visibility.to_s}\ndef \#{name}=(value)\nclass << self;\nattr_accessor \#{name.inspect}\nend\n@\#{name} = value\nend\n" 230: else 231: klass.class_eval "\#{writer_visibility.to_s}\ndef \#{name}=(value)\n\#{instance_variable_name} = value\nend\n" 232: end 233: rescue SyntaxError 234: raise SyntaxError.new(column) 235: end