Replicator – jak nejlépe uložit data

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
kolsi
Člen | 131
+
0
-

Ahoj,

mám ve formuláři replikátor:

		$contactTypes = $this->contactRepository->getContactTypes();
		$replicator = $form->addDynamic("contacts", function (Container $container) use ($contactTypes, $self) {
			$container->addHidden('id');
			$container->addSelect('type_id', 'Type', $contactTypes);
			$container->addText('value', 'Value');
			$container->addSubmit('remove', 'Remove')
					->setAttribute('class', 'ajax remove')
					->setValidationScope(FALSE)
					->addRemoveOnClick(function ($companies, $container) use($self) {
						$self->invalidateControl('contacts');
					});
		}, 0);

		// tlačítko pro přidání nového kontaktu
		$replicator->addSubmit('add', 'Add new contact')
				->setAttribute('class', 'ajax add')
				->setValidationScope(FALSE)
				->addCreateOnClick(function ($replicator, $container) use ($self) {
					$self->invalidateControl('contacts');
				});

jehož data nyní potřebuji uložit. A tak se ptám, kterou byste doporučili strategii na uložení těchto dat do DB. To, co jsem často viděl, bylo, že položky se vkládaly/odebíraly z DB přímo v handleru daných tlačítek. Což já ale nechci, já je chci uložit až po odeslání formuláře stiskem tlačítka SAVE. V té době však ale už nevím, které položky jsou nové, které smazané a které pouze aktualizované. A tak mě napadají dvě strategie:

  1. nejdříve všechny existující položky z DB smazat, a pak je tam prostě vložit znovu, tak jak přijdou ve $form->getValues(). Něco jako mám tady:

Nevýhodu vidím v tom, že se u všech položek přečíslujou ID. Výhodou by mohla být rychlost a krátký kód.

		// uložení společností
		$this->personCompanyRepository->removePersonCompanies($this->id);
		foreach ($values->companies as $company) {
			$this->personCompanyRepository->addCompanyPerson($company->id, $this->id);
		}
  1. ve „for“ cyklu postupně kontrolovat, jestli je daná položka v DB, pokud ano pak aktualizovat, jinak přidat. V dalším „for“ cyklu zkontrolovat, které položky ve $form->getValues() chybí a ty z DB smazat.

Výhoda je v tom, že ID zůstávají, ale kód je o dost delší, obsahuje 2 cykly a spoustu dotazů do DB:

		// uložení kontaktů
		$current_contacts = $this->personRepository->getContacts($private_data->id);
		foreach ($values->contacts as $index => $contact) {

			if (empty($contact->id)) { // přidání nového kontaktu
				$this->personRepository->addContact($private_data->id, $contact->type_id, $contact->value);
			} else { // editace již existujícího kontaktu
				$current_contact = $current_contacts->get($contact->id);
				if ($current_contact->type_id != $contact->type_id || $current_contact->value != $contact->value) {
					$current_contact->update(array('type_id' => $contact->type_id, 'value' => $contact->value));
				}
			}
		}

		// ještě vymažeme smazané kontakty
		foreach ($current_contacts as $current_contact) {
			// tohle zatím nefunguje dobře, protože klíče ve $values->contacts nemusí odpovídat ID v DB, a tak zřejmě bude chtít další sekvenční vyhledávání :(
			if (!isset($values->contacts[$current_contact->id])) {
				$current_contact->delete();
			}
		}

Napadá vás ještě nějaká jiná možnost nebo jak byste řešili vy, že se změny do DB nezapíšou už při stisku Přidat/Odebrat, ale až po odeslání kompletního formuláře?

voda
Člen | 561
+
0
-

Ahoj,

načti si nejdřív všechny kontakty z db, porovnej s údaji z formuláře (můžeš použít třeba arrray_diff) a staré položky z db smaž jedním delete dotazem, nové položky (nebo upravené) vlož do db.

kolsi
Člen | 131
+
0
-

Díky, takže v podstatě nějak takto?

	// uložení kontaktů
		$current_contacts = $this->personRepository->getContacts($private_data->id);
		$result = array_udiff($current_contacts->fetchPairs('id'), (array)$values->contacts, function($a, $b) { return $a->id - $b->id;	});
		if(!empty($result)) {
			$current_contacts->where('id', $result)->delete();
		}

		foreach ($values->contacts as $index => $contact) {
			if (empty($contact->value))
				continue;

			if (empty($contact->id)) { // přidání nového kontaktu
				$this->personRepository->addContact($private_data->id, $contact->type_id, $contact->value);
			} else { // editace již existujícího kontaktu
				$current_contacts = $this->personRepository->getContacts($private_data->id);	// aby nešlo podstrčit ID cizí osoby
				$current_contacts->where('id', $contact->id)->update(array('type_id' => $contact->type_id, 'value' => $contact->value));
			}
		}
voda
Člen | 561
+
0
-

V poslední else větvi (editace již existujícího kontaktu) ještě zruš duplicitní načítání $current_contacts a aktualizuj jen ty položky které mají jiné type_id nebo value.

kolsi
Člen | 131
+
0
-
$current_contacts = $this->personRepository->getContacts($private_data->id);

Tohle tam mám schválně, jinak se to následné „where“ řetězí za podmínku, která vznikla při mazání na začátku.

voda
Člen | 561
+
0
-

Tak tam zkus přidat clone (nevím jestli to funguje), nebo volej přímo update. Načitat data která už máš (a navíc v cyklu) je zbytečné.

kolsi
Člen | 131
+
0
-

Já nevím, jestli jsem to dobře pochopil, ale myslel jsem, že to funguje tak, že ono to stejně žádná data nenačte, ale teprve až když zavolám to update(…), tak se vytvoří dotaz do DB ze všech předchozích zřetězených where(…).

Ale jak koukám na výpis dotazů, tak to skutečně nejdřív udělá SELECT, a pak UPDATE. Zajímavé je, že bez update(…) se nevytvoří ani jeden.

SELECT *
FROM `pd2_crm_person_contact`
WHERE (`person_id` = ?) AND (`id` = ?)
parametry: 3, 40

UPDATE `pd2_crm_person_contact`
SET `type_id`='2', `value`='fgbfgbfgbfg'
WHERE (`person_id` = 3) AND (`id` = '40')

metoda getContacts je jenom

	public function getContacts($company_id) {
		return $this->connection->table("crm_person_contact")->where("person_id", $company_id);
	}

Takže ono se to zřetězí, všechno provede až v době update(…), ale stejně se rozdělí na 2 dotazy SELECT+UPDATE.

Editoval kolsi (11. 7. 2014 9:53)

voda
Člen | 561
+
0
-

Tak v tom případě to je asi v pořádku. Ale proč se provede i select nevím, zas tak dobře NDB neznám.

kolsi
Člen | 131
+
0
-

Tak v poho, ten SELECT navíc byl způsoben funkcí v našem v systému.

Zkusil jsem tu funkci udělal víc obecně, abych jí nemusel psát u každého formuláře, kde použiju Replicator.

	public function updateDynamicData($currentData, array $newData, $addCallback, $updateCallback = null, $key_name = "id") {
		$current = $currentData->fetchPairs($key_name);
		$deleted = array_udiff($current, $newData, function($a, $b) use ($key_name) { return ($a->$key_name) - $b->$key_name; });
		$added = array_udiff($newData, $current, function($a, $b) use ($key_name) { return ($a->$key_name) - $b->$key_name; });

		// přidání nových položek
		foreach($added as $newItem) {
			$addCallback($newItem);
			unset($newData[$newItem->id]);	// aby se při aktualizaci zbytečně nepletly nové položky
		}

		// smazání starých položek
		if(!empty($deleted)) {
			$_currentData = clone $currentData;
			$_currentData->where($key_name, array_keys($deleted))->delete();
		}

		// aktualizace
		if($updateCallback) {
			foreach ($newData as $index => $item) {
				$_currentData = clone $currentData;
				$updateCallback($_currentData->wherePrimary($item->$key_name), $item);
			}
		}
	}

Callbacky na update a insert by asi ani nebyly třeba a určitě by šlo vkládat rovnou z $newData (a název tabulky vytáhnout z $currentData), ale takhle to bylo rychlejší na napsání :)
Použití pak např.

		// uložení společností
		$id = $this->id;
		$this->personCompanyRepository->updateDynamicData($this->personCompanyRepository->getPersonCompanies($id, false), (array)$values->companies,
				function($added) use ($form, $id) {	$form->presenter->context->personCompanyRepository->addCompanyPerson($added->company_id, $id); },
				NULL, "company_id"
		);

nebo

		// uložení kontaktů
		$this->personRepository->updateDynamicData($this->personRepository->getContacts($private_data->id), (array)$values->contacts,
				function($added) use ($form, $private_data) { $form->presenter->context->personRepository->addContact($private_data->id, $added->type_id, $added->value); },
				function($current, $newData) { $current->where("type_id <> ? OR value <> ?", array($newData->type_id, $newData->value))->update(array('type_id' => $newData->type_id, 'value' => $newData->value)); }
		);