Multiple many-many's of the same..

Multiple many-many's of the same class

To get right to the point: SilverStripe doesn't support multiple many_many relations to the same class (read more). There is a possible solution So I've been thinking of a simple app that I thought would need them - and the possible work-arounds. I could think of only three, and then there might be a solution in the DataObject Patch that simon_w offers at the bottom of this forum post, but I didn't test that one.

  1. Use the 'reverse' relation, to create two..
  2. Use $many_many_extraFields
  3. Split the many-to-many relation into two many-to-one relations by creating a separate 'Relation Object'
  4. ...? you?

A simple example app

Consider a situation where you have a couple of houses and a couple of persons where:

  • Persons can rent and/or own multiple houses
  • Multiple persons can own or rent the same house together
  • A house may be rented by some, owned by others

Some of the things I want to be able to do:

  • Get a list of all persons that own houses.
  • Get all persons that rent and/or own a specific house
  • Get a list of all houses that are owned but not rented
  • eh...

This seems to point in the direction of at least one many-to-many relation between Persons and Houses, and possibly more, like Owners & Houses, RentPayingPersons & Houses and (why not) IllegalResidents & Houses

Options...

1. Use the 'reverse' relation, to create two..
If you only need two separate many-many relations, you could consider inversing the relationtype: have class A be many_many to class B for the first relation, and turn it around so class B is many_many for the second relation. I'm not sure about any practical implications - it feels a bit like cheating, and it does affect readability, but I guess it could work in some situations, but not im my app, since 2 probably won't be enough in the future...

2. Use $many_many_extraFields
Often you could do without any extra many-to-many relations, if you only had a way of adding some extra info to each pair of objects. The $many_many_extraFields array will let you add one or more fields to the relation table. Note that there can still only be one record for each pair in the relation table! Not good in my app, since I want to be able to have 'person1-rents-house1' as well as 'Person1-owns-House1' - and many_many will not allow that!

Besides: using the many_many_extraFields can be a bit of a hassle. Defining the fields is easy enough, but editing them from the CMS is not. AJShort's ItemSetField module (get it here), supports an ExtraFields option (amongst other things) in its ManyManyPickerField, that lets you manage them (to an extend). I'm working on a simple setup of how to use it

3. Create your own 'Relation Object'
Similar to the relation table in a many-to-many relation, you can create your own Relation Object. This means splitting up the many-to-many into two many-to-one relations. You still get the three tables, but now you can decide which relations you want to allow or not! Managing this from the CMS might be a tad more complex - but it's also quite flexible. This is what I finally ended up with.

Creating the classes

Person

class Person extends DataObject {

	static $db = array (
	  'Name' => 'Varchar(255)'
	);
	static $has_many = array (
	  'Houses' => 'Person_Houses'
	);
	static $summary_fields = array (
	  'Name' => 'Name'
	);
	static $searchable_fields = array (
	  'Name' => 'Name'
	);
}

House

class House extends DataObject {

	static $db = array (
	  'Name' => 'Varchar(255)'
	);
	static $has_many = array (
	  'Persons' => 'Person_Houses'
	);
	static $summary_fields = array (
	  'Name' => 'Name'
	);
	static $searchable_fields = array (
	  'Name' => 'Name'
	);
}

Person_Houses

class Person_Houses extends DataObject {

	static $db = array(
		'HousingType' => "Enum('none, rent, own, both', 'none')"
	);

	static $has_one = array(
		'Person' => 'Person',
		'House' => 'House'
	);

	static $summary_fields = array (
		'House.Name' => 'House',
		'HousingType' => 'Own or rent' 
	);

	static $searchable_fields = array (
	);
}

Both Person and House have a has_many relation to the object in the middle: Person_Houses. I've chosen to set up the backend from the Persons Point of view. So I can create Houses separately, and then add them to Persons, using a ComplexTableField in the Person editform. Allowing for persons to be added to houses in the House editform I'll leave out for later.

Using House.Name in the summaryfields will add the House details to the CTF alongside the Person_Houses details, hiding from the user that in fact two objects are involved.

HousingAdmin

class HousingAdmin extends ModelAdmin {

    public static $managed_models = array(
        'Person', 'House'
    );

    static $url_segment = 'housing';
    static $menu_title = 'Housing';
}

The HousingAdmin creates an extra menu option in the menubar where Houses and Persons can be managed using ModelAdmin.

Remove the ComplexTableField from the House editform

Since I don't want Persons to be added to Houses directly, I'll remove the CTF for now.

In the House class:

	function getCMSFields() {
		$fields = parent::getCMSFields();
		$fields->removeByName('Persons');
		return $fields;
	}

Manipulate the ComplexTableField in the Person editform

To be able to manipulate this CTF and it's tab a bit, I recreated it (could probably have just replaced it, but hey).

In the Person class:

	function getCMSFields() {
		$fields = parent::getCMSFields();
		$fields->removeByName('Houses');

		$fields->addFieldsToTab('Root.Houses', array( 
			$ctf = new ComplexTableField($this, 'Houses', 'Person_Houses')
		));
		$ctf->setAddTitle('house');

		return $fields;
	}

Limit possible entries

Although I want to be able to add more then one house to any single person, this should only happen if the conditions are different. I want to allow Person1 renting House1 and Person1 owning House1 at the same time, but no two similar entries should occur. The place to check this would be the onBeforeWrite() function in the Person_Houses class. There must be a far better and more sophisticated way to do this, but for now this will at least explain the principle (it's getting late):

In Person_Houses:

	function onBeforeWrite() {

		if (!$this->ID) {
			if ($id = $this->getExistingRecordID()) $this->ID = $id;  
		}
		parent::onBeforeWrite();
	}

	function getExistingRecordID() {

		$record = DataObject::get_one($this->ClassName,
			 "PersonID = '{$this->PersonID}' AND "
			."HouseID = '{$this->HouseID}' AND "
			."HousingType = '{$this->HousingType}'"
		);
		return (!empty($record))? $record->ID : 0; 
	}

Only if no ID exists - meaning a new entry - this function will look for an existing simular record, and use its ID to turn this insert into a possible update.

This is about it, I guess. There are all kinds of ways you can query this setup, either from template or from code. The one thing I don't like is the automatic use of a houses dropdown in the CTF popup, that can get awfully long. The ItemSetField could prove a solution, as it is supposed to work fine in a popup, and support pagination at the same time - but I haven't tried it that way yet.

Anyone have other, better idea's? Just let me know...

Comments

  • A possible solution with $many_many_extraFields

    class Person extends DataObject {
    static $many_many=array('House');
    static $many_many_extraFields=array(
    House=>array(relationship=>'enum("Own","Rent")')
    );
    }
    class House extends DataObject {}

    Verstuurd door Frankieboy, 06/09/2012 11:15am (2 jaar geleden)

Het versturen van reacties is uitgeschakeld.

RSS feed voor reacties op deze pagina