Completed
Push — master ( 0f709f...e669be )
by Philip
02:43
created

MySQLData::create()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 8.5806
c 0
b 0
f 0
nc 5
cc 4
eloc 27
nop 1
1
<?php
2
3
/*
4
 * This file is part of the CRUDlex package.
5
 *
6
 * (c) Philip Lehmann-Böhm <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace CRUDlex;
13
14
use Doctrine\DBAL\Query\QueryBuilder;
15
use Doctrine\DBAL\Connection;
16
17
/**
18
 * MySQL Data implementation using a given Doctrine DBAL instance.
19
 */
20
class MySQLData extends AbstractData {
21
22
    /**
23
     * Holds the Doctrine DBAL instance.
24
     */
25
    protected $database;
26
27
    /**
28
     * Flag whether to use UUIDs as primary key.
29
     */
30
    protected $useUUIDs;
31
32
    /**
33
     * Gets the many-to-many fields.
34
     *
35
     * @return array|\string[]
36
     * the many-to-many fields
37
     */
38
    protected function getManyFields() {
39
        $fields = $this->definition->getFieldNames(true);
40
        return array_filter($fields, function($field) {
41
            return $this->definition->getType($field) === 'many';
42
        });
43
    }
44
45
    /**
46
     * Gets all form fields including the many-to-many-ones.
47
     *
48
     * @return array
49
     * all form fields
50
     */
51
    protected function getFormFields() {
52
        $manyFields = $this->getManyFields();
53
        $formFields = [];
54
        foreach ($this->definition->getEditableFieldNames() as $field) {
55
            if (!in_array($field, $manyFields)) {
56
                $formFields[] = $field;
57
            }
58
        }
59
        return $formFields;
60
    }
61
62
    /**
63
     * Sets the values and parameters of the upcoming given query according
64
     * to the entity.
65
     *
66
     * @param Entity $entity
67
     * the entity with its fields and values
68
     * @param QueryBuilder $queryBuilder
69
     * the upcoming query
70
     * @param string $setMethod
71
     * what method to use on the QueryBuilder: 'setValue' or 'set'
72
     */
73
    protected function setValuesAndParameters(Entity $entity, QueryBuilder $queryBuilder, $setMethod) {
74
        $formFields = $this->getFormFields();
75
        $count      = count($formFields);
76
        for ($i = 0; $i < $count; ++$i) {
77
            $type  = $this->definition->getType($formFields[$i]);
78
            $value = $entity->get($formFields[$i]);
79
            if ($type == 'boolean') {
80
                $value = $value ? 1 : 0;
81
            }
82
            $queryBuilder->$setMethod('`'.$formFields[$i].'`', '?');
83
            $queryBuilder->setParameter($i, $value);
84
        }
85
    }
86
87
    /**
88
     * Performs the cascading children deletion.
89
     *
90
     * @param integer $id
91
     * the current entities id
92
     * @param boolean $deleteCascade
93
     * whether to delete children and sub children
94
     */
95
    protected function deleteChildren($id, $deleteCascade) {
96
        foreach ($this->definition->getChildren() as $childArray) {
97
            $childData = $this->definition->getServiceProvider()->getData($childArray[2]);
98
            $children  = $childData->listEntries([$childArray[1] => $id]);
99
            foreach ($children as $child) {
100
                $childData->doDelete($child, $deleteCascade);
101
            }
102
        }
103
    }
104
105
    /**
106
     * Checks whether the by id given entity still has children referencing it.
107
     *
108
     * @param integer $id
109
     * the current entities id
110
     *
111
     * @return boolean
112
     * true if the entity still has children
113
     */
114
    protected function hasChildren($id) {
115
        foreach ($this->definition->getChildren() as $child) {
116
            $queryBuilder = $this->database->createQueryBuilder();
117
            $queryBuilder
118
                ->select('COUNT(id)')
119
                ->from('`'.$child[0].'`', '`'.$child[0].'`')
120
                ->where('`'.$child[1].'` = ?')
121
                ->andWhere('deleted_at IS NULL')
122
                ->setParameter(0, $id);
123
            $queryResult = $queryBuilder->execute();
124
            $result      = $queryResult->fetch(\PDO::FETCH_NUM);
125
            if ($result[0] > 0) {
126
                return true;
127
            }
128
        }
129
        return false;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    protected function doDelete(Entity $entity, $deleteCascade) {
136
        $result = $this->shouldExecuteEvents($entity, 'before', 'delete');
137
        if (!$result) {
138
            return static::DELETION_FAILED_EVENT;
139
        }
140
        $id = $entity->get('id');
141
        if ($deleteCascade) {
142
            $this->deleteChildren($id, $deleteCascade);
143
        } elseif ($this->hasChildren($id)) {
144
            return static::DELETION_FAILED_STILL_REFERENCED;
145
        }
146
147
        $query = $this->database->createQueryBuilder();
148
        $query
149
            ->update('`'.$this->definition->getTable().'`')
150
            ->set('deleted_at', 'UTC_TIMESTAMP()')
151
            ->where('id = ?')
152
            ->setParameter(0, $id);
153
154
        $query->execute();
155
        $this->shouldExecuteEvents($entity, 'after', 'delete');
156
        return static::DELETION_SUCCESS;
157
    }
158
159
    /**
160
     * Gets all possible many-to-many ids existing for this definition.
161
     *
162
     * @param array $fields
163
     * the many field names to fetch for
164
     * @param $params
165
     * the parameters the possible many field values to fetch for
166
     * @return array
167
     * an array of this many-to-many ids
168
     */
169
    protected function getManyIds(array $fields, array $params) {
170
        $manyIds = [];
171
        foreach ($fields as $field) {
172
            $thisField    = $this->definition->getManyThisField($field);
173
            $thatField    = $this->definition->getManyThatField($field);
174
            $queryBuilder = $this->database->createQueryBuilder();
175
            $queryBuilder
176
                ->select('`'.$thisField.'`')
177
                ->from($field)
178
                ->where('`'.$thatField.'` IN (?)')
179
                ->setParameter(0, array_column($params[$field], 'id'), Connection::PARAM_STR_ARRAY)
180
                ->groupBy('`'.$thisField.'`')
181
            ;
182
            $queryResult = $queryBuilder->execute();
183
            $manyResults = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
184
            foreach ($manyResults as $manyResult) {
185
                if (!in_array($manyResult[$thisField], $manyIds)) {
186
                    $manyIds[] = $manyResult[$thisField];
187
                }
188
            }
189
        }
190
        return $manyIds;
191
    }
192
193
    /**
194
     * Adds sorting parameters to the query.
195
     *
196
     * @param QueryBuilder $queryBuilder
197
     * the query
198
     * @param $filter
199
     * the filter all resulting entities must fulfill, the keys as field names
200
     * @param $filterOperators
201
     * the operators of the filter like "=" defining the full condition of the field
202
     */
203
    protected function addFilter(QueryBuilder $queryBuilder, array $filter, array $filterOperators) {
204
        $i          = 0;
205
        $manyFields = [];
206
        foreach ($filter as $field => $value) {
207
            if ($this->definition->getType($field) === 'many') {
208
                $manyFields[] = $field;
209
                continue;
210
            }
211
            if ($value === null) {
212
                $queryBuilder->andWhere('`'.$field.'` IS NULL');
213
            } else {
214
                $operator = array_key_exists($field, $filterOperators) ? $filterOperators[$field] : '=';
215
                $queryBuilder
216
                    ->andWhere('`'.$field.'` '.$operator.' ?')
217
                    ->setParameter($i, $value, \PDO::PARAM_STR);
218
            }
219
            $i++;
220
        }
221
        $idsToInclude = $this->getManyIds($manyFields, $filter);
222
        if (!empty($idsToInclude)) {
223
            $queryBuilder
224
                ->andWhere('id IN (?)')
225
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
226
            ;
227
        }
228
    }
229
230
    /**
231
     * Adds pagination parameters to the query.
232
     *
233
     * @param QueryBuilder $queryBuilder
234
     * the query
235
     * @param integer|null $skip
236
     * the rows to skip
237
     * @param integer|null $amount
238
     * the maximum amount of rows
239
     */
240
    protected function addPagination(QueryBuilder $queryBuilder, $skip, $amount) {
241
        $queryBuilder->setMaxResults(9999999999);
242
        if ($amount !== null) {
243
            $queryBuilder->setMaxResults(abs(intval($amount)));
244
        }
245
        if ($skip !== null) {
246
            $queryBuilder->setFirstResult(abs(intval($skip)));
247
        }
248
    }
249
250
    /**
251
     * Adds sorting parameters to the query.
252
     *
253
     * @param QueryBuilder $queryBuilder
254
     * the query
255
     * @param string|null $sortField
256
     * the sort field
257
     * @param boolean|null $sortAscending
258
     * true if sort ascending, false if descending
259
     */
260
    protected function addSort(QueryBuilder $queryBuilder, $sortField, $sortAscending) {
261
        if ($sortField !== null) {
262
263
            $type = $this->definition->getType($sortField);
264
            if ($type === 'many') {
265
                $sortField = $this->definition->getInitialSortField();
266
            }
267
268
            $order = $sortAscending === true ? 'ASC' : 'DESC';
269
            $queryBuilder->orderBy('`'.$sortField.'`', $order);
270
        }
271
    }
272
273
    /**
274
     * Adds the id and name of referenced entities to the given entities. The
275
     * reference field is before the raw id of the referenced entity and after
276
     * the fetch, it's an array with the keys id and name.
277
     *
278
     * @param Entity[] &$entities
279
     * the entities to fetch the references for
280
     * @param string $field
281
     * the reference field
282
     */
283
    protected function fetchReferencesForField(array &$entities, $field) {
284
        $nameField    = $this->definition->getReferenceNameField($field);
285
        $queryBuilder = $this->database->createQueryBuilder();
286
287
        $ids = array_map(function(Entity $entity) use ($field) {
288
            return $entity->get($field);
289
        }, $entities);
290
291
        $referenceEntity = $this->definition->getReferenceEntity($field);
292
        $table           = $this->definition->getServiceProvider()->getData($referenceEntity)->getDefinition()->getTable();
293
        $queryBuilder
294
            ->from('`'.$table.'`', '`'.$table.'`')
295
            ->where('id IN (?)')
296
            ->andWhere('deleted_at IS NULL');
297
        if ($nameField) {
298
            $queryBuilder->select('id', $nameField);
299
        } else {
300
            $queryBuilder->select('id');
301
        }
302
303
        $queryBuilder->setParameter(0, $ids, Connection::PARAM_STR_ARRAY);
304
305
        $queryResult = $queryBuilder->execute();
306
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
307
        $amount      = count($entities);
308
        foreach ($rows as $row) {
309
            for ($i = 0; $i < $amount; ++$i) {
310
                if ($entities[$i]->get($field) == $row['id']) {
311
                    $value = ['id' => $entities[$i]->get($field)];
312
                    if ($nameField) {
313
                        $value['name'] = $row[$nameField];
314
                    }
315
                    $entities[$i]->set($field, $value);
316
                }
317
            }
318
        }
319
    }
320
321
    /**
322
     * Generates a new UUID.
323
     *
324
     * @return string|null
325
     * the new UUID or null if this instance isn't configured to do so
326
     */
327
    protected function generateUUID() {
328
        $uuid = null;
329
        if ($this->useUUIDs) {
330
            $sql    = 'SELECT UUID() as id';
331
            $result = $this->database->fetchAssoc($sql);
332
            $uuid   = $result['id'];
333
        }
334
        return $uuid;
335
    }
336
337
    /**
338
     * Fetches to the rows belonging many-to-many entries and adds them to the rows.
339
     *
340
     * @param array $rows
341
     * the rows to enrich
342
     * @return array
343
     * the enriched rows
344
     */
345
    protected function enrichWithMany(array $rows) {
346
        $manyFields = $this->getManyFields();
347
        $mapping = [];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 4 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
348
        foreach ($rows as $row) {
349
            $mapping[$row['id']] = $row;
350
        }
351
        foreach ($manyFields as $manyField) {
352
            $queryBuilder = $this->database->createQueryBuilder();
353
            $nameField    = $this->definition->getManyNameField($manyField);
354
            $thisField    = $this->definition->getManyThisField($manyField);
355
            $thatField    = $this->definition->getManyThatField($manyField);
356
            $entity       = $this->definition->getManyEntity($manyField);
357
            $entityTable  = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
358
            $nameSelect   = $nameField !== null ? ', t2.`'.$nameField.'` AS name' : '';
359
            $queryBuilder
360
                ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that'.$nameSelect)
361
                ->from('`'.$manyField.'`', 't1')
362
                ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
363
                ->where('t1.`'.$thisField.'` IN (?)')
364
                ->andWhere('t2.deleted_at IS NULL');
365
            $queryBuilder->setParameter(0, array_keys($mapping), Connection::PARAM_STR_ARRAY);
366
            $queryResult    = $queryBuilder->execute();
367
            $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
368
            foreach ($manyReferences as $manyReference) {
369
                $many = ['id' => $manyReference['that']];
370
                if ($nameField !== null) {
371
                    $many['name'] = $manyReference['name'];
372
                }
373
                $mapping[$manyReference['this']][$manyField][] = $many;
374
            }
375
        }
376
        return array_values($mapping);
377
    }
378
379
    /**
380
     * First, deletes all to the given entity related many-to-many entries from the DB
381
     * and then writes them again.
382
     *
383
     * @param Entity $entity
384
     * the entity to save the many-to-many entries of
385
     */
386
    protected function saveMany(Entity $entity) {
387
        $manyFields = $this->getManyFields();
388
        $id = $entity->get('id');
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 9 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
389
        foreach ($manyFields as $manyField) {
390
            $thisField = $this->definition->getManyThisField($manyField);
391
            $thatField = $this->definition->getManyThatField($manyField);
392
            $this->database->delete($manyField, [$thisField => $id]);
393
            foreach ($entity->get($manyField) as $thatId) {
394
                $this->database->insert($manyField, [
395
                    $thisField => $id,
396
                    $thatField => $thatId['id']
397
                ]);
398
            }
399
        }
400
    }
401
402
    /**
403
     * Constructor.
404
     *
405
     * @param EntityDefinition $definition
406
     * the entity definition
407
     * @param FileProcessorInterface $fileProcessor
408
     * the file processor to use
409
     * @param $database
410
     * the Doctrine DBAL instance to use
411
     * @param boolean $useUUIDs
412
     * flag whether to use UUIDs as primary key
413
     */
414
    public function __construct(EntityDefinition $definition, FileProcessorInterface $fileProcessor, $database, $useUUIDs) {
415
        $this->definition    = $definition;
416
        $this->fileProcessor = $fileProcessor;
417
        $this->database      = $database;
418
        $this->useUUIDs      = $useUUIDs;
419
    }
420
421
    /**
422
     * {@inheritdoc}
423
     */
424
    public function get($id) {
425
        $entities = $this->listEntries(['id' => $id]);
426
        if (count($entities) == 0) {
427
            return null;
428
        }
429
        return $entities[0];
430
    }
431
432
    /**
433
     * {@inheritdoc}
434
     */
435
    public function listEntries(array $filter = [], array $filterOperators = [], $skip = null, $amount = null, $sortField = null, $sortAscending = null) {
436
        $fieldNames = $this->definition->getFieldNames();
437
438
        $queryBuilder = $this->database->createQueryBuilder();
439
        $table        = $this->definition->getTable();
440
        $queryBuilder
441
            ->select('`'.implode('`,`', $fieldNames).'`')
442
            ->from('`'.$table.'`', '`'.$table.'`')
443
            ->where('deleted_at IS NULL');
444
445
        $this->addFilter($queryBuilder, $filter, $filterOperators);
446
        $this->addPagination($queryBuilder, $skip, $amount);
447
        $this->addSort($queryBuilder, $sortField, $sortAscending);
448
449
        $queryResult = $queryBuilder->execute();
450
        $rows        = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
451
        $rows        = $this->enrichWithMany($rows);
452
        $entities    = [];
453
        foreach ($rows as $row) {
454
            $entities[] = $this->hydrate($row);
455
        }
456
        return $entities;
457
    }
458
459
    /**
460
     * {@inheritdoc}
461
     */
462
    public function create(Entity $entity) {
463
464
        $result = $this->shouldExecuteEvents($entity, 'before', 'create');
465
        if (!$result) {
466
            return false;
467
        }
468
469
        $queryBuilder = $this->database->createQueryBuilder();
470
        $queryBuilder
471
            ->insert('`'.$this->definition->getTable().'`')
472
            ->setValue('created_at', 'UTC_TIMESTAMP()')
473
            ->setValue('updated_at', 'UTC_TIMESTAMP()')
474
            ->setValue('version', 0);
475
476
477
        $this->setValuesAndParameters($entity, $queryBuilder, 'setValue');
478
479
        $id = $this->generateUUID();
480
        if ($this->useUUIDs) {
481
            $queryBuilder->setValue('`id`', '?');
482
            $uuidI = count($this->getFormFields());
483
            $queryBuilder->setParameter($uuidI, $id);
484
        }
485
486
        $queryBuilder->execute();
487
488
        if (!$this->useUUIDs) {
489
            $id = $this->database->lastInsertId();
490
        }
491
492
        $entity->set('id', $id);
493
494
        $createdEntity = $this->get($entity->get('id'));
495
        $entity->set('version', $createdEntity->get('version'));
496
        $entity->set('created_at', $createdEntity->get('created_at'));
497
        $entity->set('updated_at', $createdEntity->get('updated_at'));
498
499
        $this->saveMany($entity);
500
501
        $this->shouldExecuteEvents($entity, 'after', 'create');
502
503
        return true;
504
    }
505
506
    /**
507
     * {@inheritdoc}
508
     */
509
    public function update(Entity $entity) {
510
511
        $result = $this->shouldExecuteEvents($entity, 'before', 'update');
512
        if (!$result) {
513
            return false;
514
        }
515
516
        $formFields   = $this->getFormFields();
517
        $queryBuilder = $this->database->createQueryBuilder();
518
        $queryBuilder
519
            ->update('`'.$this->definition->getTable().'`')
520
            ->set('updated_at', 'UTC_TIMESTAMP()')
521
            ->set('version', 'version + 1')
522
            ->where('id = ?')
523
            ->setParameter(count($formFields), $entity->get('id'));
524
525
        $this->setValuesAndParameters($entity, $queryBuilder, 'set');
526
        $affected = $queryBuilder->execute();
527
528
        $this->saveMany($entity);
529
530
        $this->shouldExecuteEvents($entity, 'after', 'update');
531
532
        return $affected;
533
    }
534
535
    /**
536
     * {@inheritdoc}
537
     */
538
    public function getIdToNameMap($entity, $nameField) {
539
        $table = $this->definition->getServiceProvider()->getData($entity)->getDefinition()->getTable();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
540
        $queryBuilder = $this->database->createQueryBuilder();
541
        $nameSelect   = $nameField !== null ? ',`'.$nameField.'`' : '';
542
        $queryBuilder
543
            ->select('id'.$nameSelect)
544
            ->from('`'.$table.'`', 't1')
545
            ->where('deleted_at IS NULL');
546
        if ($nameField) {
547
            $queryBuilder->orderBy($nameField);
548
        } else {
549
            $queryBuilder->orderBy('id');
550
        }
551
        $queryResult    = $queryBuilder->execute();
552
        $manyReferences = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
553
        $result         = [];
554
        foreach ($manyReferences as $manyReference) {
555
            $result[$manyReference['id']] = $nameField ? $manyReference[$nameField] : $manyReference['id'];
556
        }
557
        return $result;
558
    }
559
560
    /**
561
     * {@inheritdoc}
562
     */
563
    public function countBy($table, array $params, array $paramsOperators, $excludeDeleted) {
564
        $queryBuilder = $this->database->createQueryBuilder();
565
        $queryBuilder
566
            ->select('COUNT(id)')
567
            ->from('`'.$table.'`', '`'.$table.'`')
568
        ;
569
570
        $deletedExcluder = 'where';
571
        $i               = 0;
572
        $manyFields      = [];
573
        foreach ($params as $name => $value) {
574
            if ($this->definition->getType($name) === 'many') {
575
                $manyFields[] = $name;
576
                continue;
577
            }
578
            $queryBuilder
579
                ->andWhere('`'.$name.'` '.$paramsOperators[$name].' ?')
580
                ->setParameter($i, $value, \PDO::PARAM_STR)
581
            ;
582
            $i++;
583
            $deletedExcluder = 'andWhere';
584
        }
585
586
        $idsToInclude = $this->getManyIds($manyFields, $params);
587
        if (!empty($idsToInclude)) {
588
            $queryBuilder
589
                ->andWhere('id IN (?)')
590
                ->setParameter($i, $idsToInclude, Connection::PARAM_STR_ARRAY)
591
            ;
592
            $deletedExcluder = 'andWhere';
593
        }
594
595
        if ($excludeDeleted) {
596
            $queryBuilder->$deletedExcluder('deleted_at IS NULL');
597
        }
598
599
        $queryResult = $queryBuilder->execute();
600
        $result      = $queryResult->fetch(\PDO::FETCH_NUM);
601
        return intval($result[0]);
602
    }
603
604
    /**
605
     * {@inheritdoc}
606
     */
607
    public function fetchReferences(array &$entities = null) {
608
        if (!$entities) {
609
            return;
610
        }
611
        foreach ($this->definition->getFieldNames() as $field) {
612
            if ($this->definition->getType($field) !== 'reference') {
613
                continue;
614
            }
615
            $this->fetchReferencesForField($entities, $field);
616
        }
617
    }
618
619
620
    /**
621
     * {@inheritdoc}
622
     */
623
    public function hasManySet($field, array $thatIds) {
624
        $thisField    = $this->definition->getManyThisField($field);
625
        $thatField    = $this->definition->getManyThatField($field);
626
        $thatEntity   = $this->definition->getManyEntity($field);
627
        $entityTable  = $this->definition->getServiceProvider()->getData($thatEntity)->getDefinition()->getTable();
628
        $queryBuilder = $this->database->createQueryBuilder();
629
        $queryBuilder
630
            ->select('t1.`'.$thisField.'` AS this, t1.`'.$thatField.'` AS that')
631
            ->from('`'.$field.'`', 't1')
632
            ->leftJoin('t1', '`'.$entityTable.'`', 't2', 't2.id = t1.`'.$thatField.'`')
633
            ->andWhere('t2.deleted_at IS NULL')
634
            ->orderBy('this, that');
635
        $queryResult  = $queryBuilder->execute();
636
        $existingMany = $queryResult->fetchAll(\PDO::FETCH_ASSOC);
637
        $existingMap  = [];
638
        foreach ($existingMany as $existing) {
639
            $existingMap[$existing['this']][] = $existing['that'];
640
        }
641
        sort($thatIds);
642
        return in_array($thatIds, array_values($existingMap));
643
    }
644
645
}
646