Doctrine EAV orderBy 和搜索

Doctrine EAV orderBy and search

我正在使用 Symfony 5 和 Doctrine 2.10 我的 Book 实体没有直接属性,所有字段都是通过 Entity-Attribute-Value 模式创建的,如下所示:

图书:

#[ORM\OneToMany(mappedBy: 'book', targetEntity: BookAttributeValue::class)]
private Collection $bookAttributeValues;

图书属性值:

#[ORM\ManyToOne(targetEntity: Book::class, inversedBy: 'bookAttributeValues')]
#[ORM\JoinColumn(nullable: false)]
private Book $book;

#[ORM\ManyToOne(targetEntity: Attribute::class)]
#[ORM\JoinColumn(nullable: false)]
private BookAttribute $bookAttribute;

#[ORM\Column(type: 'string')]
private string $value;

图书属性

#[ORM\Column(type: 'string', length: 50)]
private string $name;

#[ORM\Column(type: 'string', length: 50)]
private string $handle; // Like: NAME, DESCRIPTION, AUTHOR etc.

到 select 我在 BookRepositry 中列出的书籍:


$qb = $this->createQueryBuilder('book');

        $qb
            ->addSelect([
                'bookAttributeValues',
                'bookAttribute',
            ])
            ->leftJoin('book.attributeValues', 'bookAttributeValues')
            ->leftJoin('bookAttributeValues.bookAttribute', 'bookAttribute')
            ->andWhere(
                $qb->expr()->in('bookAttribute.handle', [
                    BookAttribute::NAME,
                    BookAttribute::DESCRIPTION,
                ]),
            );

然后我可以像这样访问我的属性:

$book = $books[0];
foreach($book->attributeValues as $value) {
   echo $value->attribute->name . ' : ' . $value->value . '<br>';
}

问题是,如何在SQL抓取状态下按名称排序?或者如何按名称搜索? Doctrine QB 会是什么样子?

DQL 关系限制

由于 EAV 使用无模式设计模式,因此没有直接的方法来使用 DQL (QueryBuilder) 通过 ORM 完成过滤或排序,作为数据库中的结果值 (Book::$attributeValues)模棱两可:

[
     ['value' => 'name_value'], 
     ['value' => 'description_value']
]

简而言之,ORM 不适用于此类“报告”。

DQL 解决方法

上述关系问题 (Book::$attributeValues) 的一种解决方法是手动映射查询构建器,以便隔离 NAME 属性和关联值,然后可用于被过滤(=IN()LIKE)或排序。

排序NAME属性值

使用AS HIDDEN添加任意别名连接列,可用于排序。

$qb = $this->createQueryBuilder('book');
$expr = $qb->expr();

$qbS = $this->_em->createQueryBuilder()
    ->select('na.id')
    ->from(BookAttribute::class, 'na')
    ->where($expr->eq('na.handle', ':attribute_name'));

$qb->addSelect([
        'bookAttributeValues',
        'bookAttribute',
        'nav.value AS HIDDEN name_value',
    ])
    ->leftJoin('book.attributeValues', 'bookAttributeValues')
    ->leftJoin('bookAttributeValues.bookAttribute', 'bookAttribute')

    //isolate the associated name attribute value as a separate column
    ->leftJoin(BookAttributeValue::class, 'nav', 'WITH', $expr->andX(
         $expr->eq('book.id', 'IDENTITY(nav.book)'),
         $expr->in('IDENTITY(nav.attribute)', $qbS->getQuery()->getDQL())
    ))
    ->andWhere($expr->in('bookAttribute.handle', ':attributes'))
    ->setParameter('attribute_name', BookAttribute::NAME)
    ->setParameter('attributes', [BookAttribute::NAME, BookAttribute::DESCRIPTION])
    ->addOrderBy('name_value')
    ->addOrderBy('a.name', 'ASC'); //Z-A (Name, Description)

NAME 属性值过滤结果

只需将条件添加到您的声明中即可。

$qb->andWhere($expr->eq('nav.value', ':attribute_value'))
    ->setParameter('attribute_value', '<desired_name_value>');

SQL 查询备选方案

由于限制,我建议将 DQL 转换为 SQL 查询,并对属性及其关联值使用单独的 nested JOIN statements。创建关系的枢轴-table。然后您可以按别名 name 连接列值进行排序。

名称属性相关值

SELECT nav.value AS name
#...
LEFT JOIN (book_attribute_value AS nav
INNER JOIN book_attribute AS na 
ON na.id = nav.attribute_id
AND na.handle = BookAttribute::NAME)
ON book.id = nav.book_id 

描述属性相关值

SELECT dav.value AS description
#...
LEFT JOIN (book_attribute_value AS dav 
INNER JOIN book_attribute AS da
ON da.id = dav.attribute_id
AND da.handle = BookAttribute::DESCRIPTION)
ON book.id = dav.book_id 

完整示例 DB-Fiddle

嵌套连接将导致关联图书的描述或名称属性值缺失 return 作为该列中的 NULL,而不是排除整行。

class BookRepository
{

    /*
     * @return array|string[][]
     */
    public function filterBooks()
    {
        $sql = <<<SQL
SELECT 
    book.*,
    nav.value AS name,
    dav.value AS description
FROM book
LEFT JOIN (book_attribute_value AS nav
INNER JOIN book_attribute AS na 
ON na.id = nav.attribute_id
AND na.handle = :attr_name)
ON book.id = nav.book_id 
LEFT JOIN (book_attribute_value AS dav 
INNER JOIN book_attribute AS da
ON da.id = dav.attribute_id
AND da.handle = :attr_descr)
ON book.id = dav.book_id 
ORDER BY name
SQL;

        $stmt = $this->_em->getConnection()->prepare($sql);
        $stmt->bindValue('attr_name', BookAttribute::NAME);
        $stmt->bindValue('attr_descr', BookAttribute::DESCRIPTION);

        return $stmt->executeQuery()->fetchAllAssociative();
    }
}

结果

id name description
1 Book_1_Name Book_1_Description
2 Book_2_Name Book_2_Description
[
   {"id": "1", "name": "Book_1_Name", "description": "Book_1_Description"},
   {"id": "2", "name": "Book_2_Name", "description": "Book_2_Description"}
]

根据需要迭代结果。

$books = $em->getRepository(Book::class)->filterBooks();
foreach ($books as $book) {
    //ksort($book, SORT_NATURAL); #optionally sort by the attribute column
    //printf('Book %s:<br>', $book['id']); #display the book id
    unset($book['id']); //remove the id column
    foreach ($book as $attribute => $value) {
        printf('%s: %s<br>', $attribute, $value);
    }
}

输出

name: Book_1_Name
description: Book_1_Description
name: Book_2_Name
description: Book_2_Description

要限制指定名称值的结果,请将 LEFT JOIN nav 更改为 INNER JOIN nav 并添加所需的条件(=IN()LIKE) 到语句的 ON 子句。

示例查询 DB-Fiddle

SELECT 
    book.*,
    nav.value AS name,
    dav.value AS description
FROM book
INNER JOIN (book_attribute_value AS nav
INNER JOIN book_attribute AS na 
ON na.id = nav.attribute_id
AND na.handle = :attr_name)
ON book.id = nav.book_id 
AND nav.value = :name_value
LEFT JOIN (book_attribute_value AS dav 
INNER JOIN book_attribute AS da
ON da.id = dav.attribute_id
AND da.handle = :attr_descr)
ON book.id = dav.book_id 

务必将值绑定到语句条件。

$stmt->bindValue('name_value', 'Book_1_Name');