此问题已作为https://github.com/graphql-python/graphene-sqlalchemy/issues/134下的GH问题发布,但我认为我也会将其发布在此处以吸引SO人群。
可以在https://github.com/somada141/demo-graphql-sqlalchemy-falcon下找到完整的工作演示。
考虑以下SQLAlchemy ORM类:
class Author(Base, OrmBaseMixin):
__tablename__ = "authors"
author_id = sqlalchemy.Column(
sqlalchemy.types.Integer(),
primary_key=True,
)
name_first = sqlalchemy.Column(
sqlalchemy.types.Unicode(length=80),
nullable=False,
)
name_last = sqlalchemy.Column(
sqlalchemy.types.Unicode(length=80),
nullable=False,
)
简单地用SQLAlchemyObjectType
包裹:
class TypeAuthor(SQLAlchemyObjectType):
class Meta:
model = Author
并透露:
author = graphene.Field(
TypeAuthor,
author_id=graphene.Argument(type=graphene.Int, required=False),
name_first=graphene.Argument(type=graphene.String, required=False),
name_last=graphene.Argument(type=graphene.String, required=False),
)
@staticmethod
def resolve_author(
args,
info,
author_id: Union[int, None] = None,
name_first: Union[str, None] = None,
name_last: Union[str, None] = None,
):
query = TypeAuthor.get_query(info=info)
if author_id:
query = query.filter(Author.author_id == author_id)
if name_first:
query = query.filter(Author.name_first == name_first)
if name_last:
query = query.filter(Author.name_last == name_last)
author = query.first()
return author
GraphQL查询,例如:
query GetAuthor{
author(authorId: 1) {
nameFirst
}
}
将导致发出以下原始SQL(取自SQLA引擎的echo日志):
SELECT authors.author_id AS authors_author_id, authors.name_first AS authors_name_first, authors.name_last AS authors_name_last
FROM authors
WHERE authors.author_id = ?
LIMIT ? OFFSET ?
2018-05-24 16:23:03,669 INFO sqlalchemy.engine.base.Engine (1, 1, 0)
正如我们所看到的那样,我们可能只需要nameFirst
字段,即name_first
列,但会获取整行。当然,GraphQL响应仅包含请求的字段,即
{
"data": {
"author": {
"nameFirst": "Robert"
}
}
}
但是我们仍然提取整行,这在处理宽表时成为一个主要问题。
有没有办法自动传达SQLAlchemy需要哪些列,以排除这种形式的过度获取?
答案 0 :(得分:0)
我的问题已在GitHub问题(https://github.com/graphql-python/graphene-sqlalchemy/issues/134)上得到解答。
我们的想法是识别info
参数(类型为graphql.execution.base.ResolveInfo
)的请求字段,该字段通过get_field_names
函数传递给解析器函数,如下所示:
def get_field_names(info):
"""
Parses a query info into a list of composite field names.
For example the following query:
{
carts {
edges {
node {
id
name
...cartInfo
}
}
}
}
fragment cartInfo on CartType { whatever }
Will result in an array:
[
'carts',
'carts.edges',
'carts.edges.node',
'carts.edges.node.id',
'carts.edges.node.name',
'carts.edges.node.whatever'
]
"""
fragments = info.fragments
def iterate_field_names(prefix, field):
name = field.name.value
if isinstance(field, FragmentSpread):
_results = []
new_prefix = prefix
sub_selection = fragments[field.name.value].selection_set.selections
else:
_results = [prefix + name]
new_prefix = prefix + name + "."
if field.selection_set:
sub_selection = field.selection_set.selections
else:
sub_selection = []
for sub_field in sub_selection:
_results += iterate_field_names(new_prefix, sub_field)
return _results
results = iterate_field_names('', info.field_asts[0])
return results
上述功能取自https://github.com/graphql-python/graphene/issues/348#issuecomment-267717809。该问题包含此功能的其他版本,但我觉得这是最完整的。
并使用标识的字段来限制SQLAlchemy查询中的检索字段:
fields = get_field_names(info=info)
query = TypeAuthor.get_query(info=info).options(load_only(*relation_fields))
应用于上述示例查询时:
query GetAuthor{
author(authorId: 1) {
nameFirst
}
}
get_field_names
函数将返回['author', 'author.nameFirst']
。然而,作为原作' SQLAlchemy ORM字段是蛇形的,需要更新get_field_names
查询以删除author
前缀并通过graphene.utils.str_converters.to_snake_case
函数转换字段名。
长话短说,上面的方法产生了一个原始SQL查询,如下所示:
INFO:sqlalchemy.engine.base.Engine:SELECT authors.author_id AS authors_author_id, authors.name_first AS authors_name_first
FROM authors
WHERE authors.author_id = ?
LIMIT ? OFFSET ?
2018-06-09 13:22:16,396 INFO sqlalchemy.engine.base.Engine (1, 1, 0)
<强>更新强>
如果有人在这里想到了实施,我就开始实施我自己版本的get_query_fields
功能:
from typing import List, Dict, Union, Type
import graphql
from graphql.language.ast import FragmentSpread
from graphql.language.ast import Field
from graphene.utils.str_converters import to_snake_case
import sqlalchemy.orm
from demo.orm_base import OrmBaseMixin
def extract_requested_fields(
info: graphql.execution.base.ResolveInfo,
fields: List[Union[Field, FragmentSpread]],
do_convert_to_snake_case: bool = True,
) -> Dict:
"""Extracts the fields requested in a GraphQL query by processing the AST
and returns a nested dictionary representing the requested fields.
Note:
This function should support arbitrarily nested field structures
including fragments.
Example:
Consider the following query passed to a resolver and running this
function with the `ResolveInfo` object passed to the resolver.
>>> query = "query getAuthor{author(authorId: 1){nameFirst, nameLast}}"
>>> extract_requested_fields(info, info.field_asts, True)
{'author': {'name_first': None, 'name_last': None}}
Args:
info (graphql.execution.base.ResolveInfo): The GraphQL query info passed
to the resolver function.
fields (List[Union[Field, FragmentSpread]]): The list of `Field` or
`FragmentSpread` objects parsed out of the GraphQL query and stored
in the AST.
do_convert_to_snake_case (bool): Whether to convert the fields as they
appear in the GraphQL query (typically in camel-case) back to
snake-case (which is how they typically appear in ORM classes).
Returns:
Dict: The nested dictionary containing all the requested fields.
"""
result = {}
for field in fields:
# Set the `key` as the field name.
key = field.name.value
# Convert the key from camel-case to snake-case (if required).
if do_convert_to_snake_case:
key = to_snake_case(name=key)
# Initialize `val` to `None`. Fields without nested-fields under them
# will have a dictionary value of `None`.
val = None
# If the field is of type `Field` then extract the nested fields under
# the `selection_set` (if defined). These nested fields will be
# extracted recursively and placed in a dictionary under the field
# name in the `result` dictionary.
if isinstance(field, Field):
if (
hasattr(field, "selection_set") and
field.selection_set is not None
):
# Extract field names out of the field selections.
val = extract_requested_fields(
info=info,
fields=field.selection_set.selections,
)
result[key] = val
# If the field is of type `FragmentSpread` then retrieve the fragment
# from `info.fragments` and recursively extract the nested fields but
# as we don't want the name of the fragment appearing in the result
# dictionary (since it does not match anything in the ORM classes) the
# result will simply be result of the extraction.
elif isinstance(field, FragmentSpread):
# Retrieve referened fragment.
fragment = info.fragments[field.name.value]
# Extract field names out of the fragment selections.
val = extract_requested_fields(
info=info,
fields=fragment.selection_set.selections,
)
result = val
return result
将AST解析为dict
,保留查询结构,并(希望)匹配ORM的结构。
运行查询的info
对象,如:
query getAuthor{
author(authorId: 1) {
nameFirst,
nameLast
}
}
产生
{'author': {'name_first': None, 'name_last': None}}
虽然这是一个更复杂的查询:
query getAuthor{
author(nameFirst: "Brandon") {
...authorFields
books {
...bookFields
}
}
}
fragment authorFields on TypeAuthor {
nameFirst,
nameLast
}
fragment bookFields on TypeBook {
title,
year
}
产生
{'author': {'books': {'title': None, 'year': None},
'name_first': None,
'name_last': None}}
现在,这些词典可用于定义主表(在这种情况下为Author
)上的字段,因为它们的值为None
,例如{{1或{@ 1}}关系中该主要表格的关系字段,例如字段name_first
。
自动应用这些字段的简单方法可以采用以下函数的形式:
title