Multivalue Fields Development Plan
Contact
- Ingo Schommer (ingo at silverstripe dot com)
- Sam Minnee (sam at silverstripe dot com)
- Mark Rickerby
Status
- Alpha in development branch
- See GeoPoint? class in new GIS branch ( http://open.silverstripe.com/browser/modules/gis/trunk/code/model/fieldtypes/GeoPoint.php)
Current Shortcomings
The current assumption is that DBFields have a single representation of an atomic value (e.g. a piece of text or a number) which maps directly to a database column.
Current API: Getting data
- ViewableData::obj()
- DataObject::dbObject()
- ViewableData::getField() (with casting and caching)
Current API: Setting data
- DataObject::__construct()
- DataObject::update()
- DataObject::castedUpdate()
- DataObject::populateDefaults()
- DataObject::write() -> DBField::writeToManipulation()
- ViewableData::setField() (with casting and caching)
Glossary
- Database Field: DBField on the object with a type and optionally a value. Might be atomic or composite.
- Custom Database Field: DBField which is defined on the inspected class, and stored in a table related to this class
- Database Column: Actual column in the database, always holding one distinct value. A "Database Field" might be composed of multiple database columns. Shouldn't not be used for any direct data retrieval or manipulation, please use DBField::getValue() and DBField::setValue().
- Record:
- Atomic Value: A value which can be represented in a scalar datatype (string/int/float)
- Composite Value: A value which has multiple representations, and is stored in an array or object. The DBField knows how to compose and decompose between an atomic value and its composite.
Requirements
A DBField should encapsulate access to the database-layer - the DataObject? shouldn't need to know about database columns and solely interface with DBField::getValue() and DBField::setValue(). The database manipulation API (DataObject::write()) triggers the DBField to get appropriate SQL statements. With this assumption, a DBField can manage complex types which have one or more of the following conditions:
- Multiple representations in the ORM, but single value in the database (Example: GIS Point with binary database storage, but "X" and "Y" coordinates through DBField)
- Atomic default representation in the ORM, but multiple values in the database (Example: Currency with a database column for "number" and "currency type", but a single composed string through DBField).
The idea of multi-value fields expands to casted objects without any database representation (ViewableData?).
We will refactor the current DBField implementation to support multivalue setting/getting through the existing API.
Gettting Data
- DataObject::construct() accepts a hashed map of SQL-columns, but doesn't know which one belong to which DBField. It passes the whole record to DBField which can "pick'n'choose".
- DataObject? doesn't construct/store casted DBFields by default, it retaints the record from SQLQuery. The DBField can decide which values to choose in setValue()
Setting Data
- Form->saveInto() calls FormField?->saveInto() calls DataObject?->setCastedField($name,$value) calls FormField?->dataValue()
- DBField::setValue() accepts only one mixed parameter
- FormFields? should preferrably use a special setter if they know about the datastructure they want to save, e.g. GeoPointField? would trigger GeoPoint?->setValueByCoordinates(x,y)
- DataObject? defaults should be accepted in a multivalue format
- DBField should have defaults if a scalar value is used (e.g. "NZD" for a currency-type)
- DBField can be instanciated through specialized factory methods, e.g. GeoPoint::create_from_coordinates()
Change Detection
- A DBField has to know its DataObject? context, so it can trigger the DataObject? change detection on setValue(). The DataObject? context is optional (DBField could be instanciated as casted unstored value through a single).
Schema
- DataObjectDecorators? such as Versioned have to know which actual colums are in the database.
- DBField->databaseColumns()
- DataObject?->hasDatabaseColumn($fieldName)
- DataObject?->databaseColumns(): gets DataObject?->databaseFields() and calls DBField->databaseColumns() on all applicable instances (excluding relation-columns)
- DataObject?->customDatabaseColumns(): gets DataObject?->customDatabaseFields() for this table and calls DBField->databaseColumns() on all applicable instances (excluding relation-columns)
Datamodel Customization
- Manually constructing a SQLQuery has to use DBField->addToQuery instead of manually filling the SELECT. SELECT Currency FROM MyObject is transformed to SELECT Currency, Currency_Type FROM MyObject if you want to access a valid Currency field.
Legacy Handling
Testing (order of priority)
- Object cache in ViewableData::obj()
- Casting cache in ViewableData::obj()
- Schema creation for multivalue fields (DBField::requireFields())
- Failover handling
- Casting of direct $db fields on DataObject?
- Casting of implied fields through custom getters on DataObject?
- DBField::writeToManipulation()
- DBField::addToQuery()
- Change detection on atomic fields
- Change detection on composite fields
TODO
- Make Versioned, Translatable compatible
- Refactor CurrencyField? to optionally support Multivalue Fields
- Make Currency a Multivalue Field
- Refactor HTMLText
- Refactor PasswordField?
- Testing (see above)
Use Cases
Currency
- $myCurrency->getValue() returns "€9.99"
- $myCurrency->getCurrency() returns "EUR"
- $myCurrency->RAW() returns "9.99"
- database columns: MyCurrency? (Value: 9.99) and MyCurrency_Currency (Value: EUR)
- $myCurrency->setValue('9.99') will set a default of "EUR"
- $myCurrency->setValue('9.99','EUR')
GeoPoint?
Stores a GIS Point as a single database column with binary data. Can be retrieved as a string with the GIS-SQL command "AsText?()" and stored with "GeomFromText?()".
- $myPoint->getValue() returns "POINT(2 3)"
- $myPoint->getX() returns "2" (through regex-parsing)
- $myPoint->getCoordinates returns array(2,3) (through regex parsing)
- $myPoint->setValue(2,3)
- $myPoint->setValue(array(2,3))
- GeoPoint::from_xy(2,3)
- $myPoint->writeToManipulation() will compose value and save GeomFromText?('POINT(2 3)')
- $myPoint->addToQuery() will add "AsText?(MyPoint?') AS MyPoint_AsText" to the SELECT statement
PasswordField?
Store Password, EncryptionType and Salt separately.
HTMLText
Store Content and ContentFormat (e.g. "Plain", "HTML", "BBCode", "Textile", ...) separately. Rename HTMLText to FormattedText
