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');
我正在使用 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');