简体   繁体   English

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

[英]Creating Dynamic Schema on Runtime Graphene

I almost spent 3 days to find a way for creating a dynamic schema in python graphene.我几乎花了 3 天的时间来寻找一种在 python 石墨烯中创建动态模式的方法。 the only related result I could find is the below link: https://github.com/graphql-python/graphene/blob/master/graphene/types/dynamic.py But I couldn't find any documentation for it.我能找到的唯一相关结果是以下链接: https ://github.com/graphql-python/graphene/blob/master/graphene/types/dynamic.py 但我找不到任何文档。

The whole idea is to create a dynamic schema.整个想法是创建一个动态模式。 I want to provide a GraphQL compatible API that makes users able to query my contents even if Models are not defined in the code.我想提供一个兼容 GraphQL 的 API,即使代码中没有定义模型,用户也可以查询我的内容。 In other words, I want to create Models on the fly.换句话说,我想动态创建模型。 I have no idea about what shall I do.我不知道我该怎么做。

It would be a great favor if you can provide an example for that.如果你能为此提供一个例子,那将是一个很大的帮助。

Update :更新 :

My Project is a Headless CMS which has a feature that users can create their own content types and I want to provide a GraphQL interface to make everything easier and more flexible.我的项目是一个无头 CMS,它具有用户可以创建自己的内容类型的功能,我想提供一个 GraphQL 界面来让一切变得更容易和更灵活。

Here is example of my Content Types in DB :这是我在 DB 中的内容类型的示例:

{
  "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"
    }
  ]
}

And Here is Stored content in DB based on that content type :这是基于该内容类型在数据库中存储的内容:

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

Now as my Models are not declared in Code and they are completely in DB, I want to make my schemas on the fly and I don't know what is best the solution for this.现在,由于我的模型没有在代码中声明并且它们完全在数据库中,我想即时制作我的模式,我不知道什么是最好的解决方案。 I know there should be a way because the other Headless CMS Projects are providing this.我知道应该有一种方法,因为其他无头 CMS 项目正在提供这种方法。

Thanks in advance!提前致谢!

Basically, schema is created something like this:基本上,架构是这样创建的:

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

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

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

First, in order to add some kind of dynamics, you will most likely want to wrap the above code in a function like create_schema() .首先,为了添加某种动态,您很可能希望将上述代码包装在像create_schema()这样的函数中。

Then, when you want to dynamically create a class during runtime, the above code can be rewritten like this:然后,当你想在运行时动态创建一个类时,上面的代码可以改写成这样:

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])

For your example it could look something like this:对于您的示例,它可能看起来像这样:

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()))

Note that if you try to insert new fields into already existing class, like this - MyType.another_field = graphene.String() , then it won't work: that is because when graphene.ObjectType class is instantiated, all its fields are recorded in self._meta.fields OrderedDict.请注意,如果您尝试将新字段插入到已经存在的类中,就像这样 - MyType.another_field = graphene.String() ,那么它将不起作用:这是因为当graphene.ObjectType类被实例化时,它的所有字段都会被记录在self._meta.fields OrderedDict。 And updating it is not as straightforward as just MyType._meta.fields['another_field'] = thefield - see the code of graphene.ObjectType.__init_subclass_with_meta__ for details.并且更新它并不像MyType._meta.fields['another_field'] = thefield - 有关详细信息,请参阅graphene.ObjectType.__init_subclass_with_meta__的代码。

So if your schema is dynamically changed then it might be better to fully re-create it from scratch than to patch it.因此,如果您的架构是动态更改的,那么从头开始完全重新创建它可能比修补它更好。

I'd like to share another neat method.我想分享另一种巧妙的方法。

So, the issue is that the graphene.ObjectType is not a regular Python class.因此,问题在于 graphene.ObjectType 不是常规的 Python 类。 It has a special metaclass that you can see implemented here .它有一个特殊的元类,您可以在这里看到它的实现。 At the moment Python takes care of the inheritance process (when the class itself is initalized), graphene does some operations to register the type.在 Python 负责继承过程(当类本身被初始化时)时,石墨烯会执行一些操作来注册类型。 I didn't find a way to change types after the inheritance happens.在继承发生后我没有找到改变类型的方法。 However, if you just want to generate the schema out of a predefined boilerplate (like me) or some other source, you can do something like this.但是,如果您只想从预定义的样板(如我)或其他来源生成模式,您可以执行类似的操作。 I first define a handy inherit method:我首先定义一个方便的继承方法:

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

Now the key is to perform the inheritance when the type's class is not about to change anymore.现在的关键是当类型的类不再改变时执行继承。

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)

Where I got with my little library was generating everything from a boilerplate class like this:我用我的小图书馆得到的东西是从这样的样板类生成的:

@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
        }

Biggest challenge were the NestedFields and figuring out automatic Django ORM query select/prefetch when a request comes in, but I won't go into detail unless that's something relevant.最大的挑战是 NestedFields 并在请求进入时找出自动 Django ORM 查询选择/预取,但除非相关,否则我不会详细介绍。

My solution for django.我的 django 解决方案。

Dynamic search by application included in INSTALLED_APPS and graphql schema collection. INSTALLED_APPS 和 graphql 模式集合中包含的应用程序动态搜索。 You should stick to the common name in mutation or queries (it should contain a common occurrence of the word by which we will find the class [name_class_contains]. example: AnyNameMutationMixin => word MutationMixin must be present in all classes name that you want to add to mutate) you must also have the same paths names.py and mutate.py in applications您应该坚持突变或查询中的通用名称(它应该包含我们将找到类 [name_class_contains] 的常见单词。例如:AnyNameMutationMixin => 单词 MutationMixin 必须出现在您想要的所有类名称中添加到 mutate)您还必须在应用程序中具有相同的路径 names.py 和 mutate.py

file dynamic_collect_graphene_scheme文件 dynamic_collect_graphene_scheme

import importlib
import inspect

import graphene

from django.conf import settings


def dynamic_inherit(name_cls: str, parent_class_list):
    """ name_cls needed to keep the standard names Mutation и Query"""

    class Mutate(*parent_class_list, graphene.ObjectType):
        pass

    class Query(*parent_class_list, graphene.ObjectType):
        pass

    if name_cls.lower() == "mutate":
        return Mutate
    elif name_cls.lower() == "query":
        return Query
    else:
        raise ValueError('cls need choice [mutate,query]')


def collect_class(name_class_contains, path_module_from_app):
    list_cls = []
    for application in settings.INSTALLED_APPS:
        application = application.split('.')[0]  # if app.AppConfig del AppConfig
        try:
            file = importlib.import_module(application + path_module_from_app)
        except ImportError:
            pass
        else:
            all_class = inspect.getmembers(file, inspect.isclass)
            for cls in all_class:
                if cls[0].find(name_class_contains) != -1:
                    list_cls.append(cls[1])
    return list_cls

file schema.py文件 schema.py

"""Global GraphQL Schema"""
from graphene import Schema

from main.dynamic_collect_graphene_scheme import dynamic_inherit, collect_class

schema = Schema(query=dynamic_inherit("query", collect_class(name_class_contains="MixinQuery",
                                                             path_module_from_app=".api.queries")),
                mutation=dynamic_inherit("mutate", collect_class(name_class_contains="MixinMutation",
                                                                 path_module_from_app=".api.mutate")))

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM