在运行时石墨烯上创建动态模式

Creating Dynamic Schema on Runtime Graphene

我几乎花了 3 天时间才找到一种在 python 石墨烯中创建动态模式的方法。 我能找到的唯一相关结果是下面的 link: https://github.com/graphql-python/graphene/blob/master/graphene/types/dynamic.py 但是我找不到它的任何文档。

整个想法是创建一个动态模式。我想提供一个兼容 GraphQL 的 API,即使模型没有在代码中定义,用户也可以查询我的内容。换句话说,我想即时创建模型。我不知道该怎么办。

如能提供示例,将不胜感激。

更新:

我的项目是一个 Headless CMS,它具有用户可以创建自己的内容类型的功能,我想提供一个 GraphQL 接口,使一切变得更简单、更灵活。

这是我在数据库中的内容类型示例:

{
  "id": "author",
  "name": "Book Author",
  "desc": "",
  "options":[
    {
      "id": "author_faname",
      "label": "Sample Sample",
      "type": "text",
      "required": true,
      "placeholder":"One Two Three Four"
    },
    {
      "id": "author_enname",
      "label": "Sample label",
      "type": "text",
      "required": true,
      "placeholder":"Sample Placeholder"
    }
  ]
}

这是基于该内容类型在数据库中存储的内容:

{
  "id": "9rqgbrox10",
  "content_type": "author",
  "data":{
    "author_fname":"Jimmy",
    "author_ename":"Hello"
  }
}

现在,由于我的模型没有在代码中声明,它们完全在数据库中,我想即时制作我的模式,但我不知道什么是最好的解决方案。我知道应该有一种方法,因为其他 Headless CMS 项目正在提供这种方法。

提前致谢!

基本上,架构是这样创建的:

class MyType(graphene.ObjectType):
    something = graphene.String()

class Query(graphene.ObjectType):
    value = graphene.Field(MyType)

schema = graphene.Schema(query=Query, types=[MyType])

首先,为了添加某种动态,您很可能希望将上述代码包装在函数中,例如 create_schema()

然后,当你想在运行时动态创建一个class,上面的代码可以改写成这样:

def create_schema():
    MyType = type('MyType', (graphene.ObjectType,), {
        'something': graphene.String(),
    })

    Query = type('Query', (graphene.ObjectType,), {
        'value': graphene.Field(MyType),
    })

    return graphene.Schema(query=Query, types=[MyType])

对于您的示例,它可能看起来像这样:

def make_resolver(record_name, record_cls):
    def resolver(self, info):
        data = ...
        return record_cls(...)
    resolver.__name__ = 'resolve_%s' % record_name
    return resolver

def create_schema(db):
    record_schemas = {}
    for record_type in db.get_record_types():
        classname = record_type['id'].title()  # 'Author'
        fields = {}
        for option in record_type['options']:
            field_type = {
                'text': graphene.String,
                ...
            }[option['type']
            fields[option['id']] = field_type()  # maybe add label as description?
        rec_cls = type(
            classname,
            (graphene.ObjectType,), 
            fields,
            name=record_type['name'],
            description=record_type['desc'],
        )
        record_schemas[record_type['id']] = rec_cls

    # create Query in similar way
    fields = {}
    for key, rec in record_schemas:
        fields[key] = graphene.Field(rec)
        fields['resolve_%s' % key] = make_resolver(key, rec)
    Query = type('Query', (graphene.ObjectType,), fields)

    return graphene.Schema(query=Query, types=list(record_schemas.values()))

请注意,如果您尝试将新字段插入现有 class, 像这样 - MyType.another_field = graphene.String(), 那么它将不起作用:那是因为当 graphene.ObjectType class 被实例化时, 它的所有字段都记录在 self._meta.fields OrderedDict 中。 并且更新它并不像 MyType._meta.fields['another_field'] = thefield 那样简单-有关详细信息,请参阅 graphene.ObjectType.__init_subclass_with_meta__ 的代码。

因此,如果您的架构是动态更改的,那么从头开始完全重新创建它可能比修补它更好。

我想分享另一种巧妙的方法。

因此,问题在于 graphene.ObjectType 不是常规 Python class。它有一个特殊的metaclass that you can see implemented here。目前 Python 负责继承过程(当 class 本身被初始化时),石墨烯会做一些操作来注册类型。继承发生后,我没有找到更改类型的方法。但是,如果您只想从预定义的样板文件(如我)或其他来源生成模式,您可以这样做。我首先定义一个方便的继承方法:

def inherit_from(Child, Parent, persist_meta=False):
    """Return a class that is equivalent to Child(Parent) including Parent bases."""
    PersistMeta = copy(Child.Meta) if hasattr(Child, 'Meta') else None

    if persist_meta:
        Child.Meta = PersistMeta

    # Prepare bases
    child_bases = inspect.getmro(Child)
    parent_bases = inspect.getmro(Parent)
    bases = tuple([item for item in parent_bases if item not in child_bases]) + child_bases

    # Construct the new return type
    try:
        Child = type(Child.__name__, bases, Child.__dict__.copy())
    except AttributeError as e:
        if str(e) == 'Meta':
            raise AttributeError('Attribute Error in graphene library. Try setting persist_meta=True in the inherit_from method call.')
        raise e

    if persist_meta:
        Child.Meta = PersistMeta

    return Child

现在的关键是在类型的 class 不再更改时执行继承。

def context_resolver_factory(attr):
    """Create a simple resolver method with default return value None."""

    def resolver(obj, info):
        return info.context.get(attr, None)

    return resolver


class User:
    id = graphene.ID()
    name = graphene.String(resolver=lambda user, info: user.name)


class Query: pass
    me = graphene.Field(User)

    def resolve_me(self, info):
        return info.context["user"]


inherit_from(User, graphene.ObjectType)  # no changes to User class are possible after this line

# method 1: sometimes it's really neat and clean to include a resolver in the field definition
setattr(Query, 'user', graphene.User(resolver=context_resolver_factory('user'))
# or even use lambda if a factory is still overkill
setattr(Query, 'user', graphene.User(resolver=lambda query, info: info.context["user"]))


# method 2: if you want to set the resolver separately, you can do it this way
setattr(Query, 'user', graphene.User())
setattr(Query, 'resolve_user', context_resolver_factory('user'))

# any changes to `Query.Meta` can be done here too

inherit_from(Query, graphene.ObjectType)  # no changes to Query class are possible after this line

schema = graphene.Schema(query=Query)

我的小图书馆从样板 class 生成所有内容,如下所示:

@register_type('Product')
class ProductType:
    class Meta:
        model = Product
        fields = '__all__'
        related_fields = {
            NestedField('tags', TagType),
            NestedField('related_products', 'self'),
        }
        lookups = {
            'id': graphene.ID(),
            'name': graphene.String(description="Name"),
            'ean': graphene.String(),
            'brand': graphene.String()
        }
        filters = {
            'ids': IDFilter,
            'django_filter': DjangoFilter,
            'pagination': PaginationFilter,
            'search_name': ProductMLNSearchFilter
        }

最大的挑战是 NestedFields 和在收到请求时找出自动 Django ORM 查询 select/prefetch,但除非相关,否则我不会详细介绍。