与Graphene-SQLAlchemy接口类加Node接口的ID冲突

时间:2019-06-04 13:52:46

标签: sqlalchemy graphql graphene-python

我已经为使用SQLalchemy的数据库中表示的多态实体编写了Graphene模型。

问题很简单:

我想创建一个反映我的Graphene SQLAlchemy模型的接口,而且a)实现Node或b)不与Node冲突,并允许我检索模型的ID,而无需在Node {id上添加... }到查询字符串。

必须从我的基于ORM的接口中排除ID字段或与Node接口冲突的字段,这样做是为了获取ID,然后需要在Node {id上添加...,这很丑陋。

我创建了一个扩展了Graphene.Interface的SQLAlchemyInterface对象。我的许多(但不是全部)模型都使用了此模型,也使用Node作为接口。第一个问题是它包含一个ID字段,并且与Node接口冲突。

我排除了id字段以免干扰Node,但是发现我无法再直接在模型上查询ID,因此不得不在Node {id}上向查询字符串添加...。

然后,我决定使用此SQLAlchemyInterface扩展Node。我不喜欢这种方法,因为我需要为所有不需要实现SQLAlchemyInterface的模型使用另一个(命名)Node接口

function getMorphType($typeOrClass)
{
    $morphMap = array_flip(\Illuminate\Database\Eloquent\Relations\Relation::morphMap());

    return array_get($morphMap, $typeOrClass, $typeOrClass);
}

接口隐含,模型+查询代码示例:

class SQLAlchemyInterface(Node):
    @classmethod
    def __init_subclass_with_meta__(
            cls,
            model=None,
            registry=None,
            only_fields=(),
            exclude_fields=(),
            connection_field_factory=default_connection_field_factory,
            _meta=None,
            **options
    ):
        _meta = SQLAlchemyInterfaceOptions(cls)
        _meta.name = f'{cls.__name__}Node'

        autoexclude_columns = exclude_autogenerated_sqla_columns(model=model)
        exclude_fields += autoexclude_columns

        assert is_mapped_class(model), (
            "You need to pass a valid SQLAlchemy Model in " '{}.Meta, received "{}".'
        ).format(cls.__name__, model)

        if not registry:
            registry = get_global_registry()

        assert isinstance(registry, Registry), (
            "The attribute registry in {} needs to be an instance of "
            'Registry, received "{}".'
        ).format(cls.__name__, registry)

        sqla_fields = yank_fields_from_attrs(
            construct_fields(
                model=model,
                registry=registry,
                only_fields=only_fields,
                exclude_fields=exclude_fields,
                connection_field_factory=connection_field_factory
            ),
            _as=Field
        )
        if not _meta:
            _meta = SQLAlchemyInterfaceOptions(cls)
        _meta.model = model
        _meta.registry = registry
        connection = Connection.create_type(
            "{}Connection".format(cls.__name__), node=cls)
        assert issubclass(connection, Connection), (
            "The connection must be a Connection. Received {}"
        ).format(connection.__name__)
        _meta.connection = connection
        if _meta.fields:
            _meta.fields.update(sqla_fields)
        else:
            _meta.fields = sqla_fields
        super(SQLAlchemyInterface, cls).__init_subclass_with_meta__(_meta=_meta, **options)

    @classmethod
    def Field(cls, *args, **kwargs):  # noqa: N802
        return NodeField(cls, *args, **kwargs)

    @classmethod
    def node_resolver(cls, only_type, root, info, id):
        return cls.get_node_from_global_id(info, id, only_type=only_type)

    @classmethod
    def get_node_from_global_id(cls, info, global_id, only_type=None):
        try:
            node: DeclarativeMeta = one_or_none(session=info.context.get('session'), model=cls._meta.model, id=global_id)
            return node
        except Exception:
            return None

    @staticmethod
    def from_global_id(global_id):
        return global_id

    @staticmethod
    def to_global_id(type, id):
        return id

模式:

class CustomNode(Node):
    class Meta:
        name = 'UuidNode'

    @staticmethod
    def to_global_id(type, id):
        return '{}:{}'.format(type, id)

    @staticmethod
    def get_node_from_global_id(info, global_id, only_type=None):
        type, id = global_id.split(':')
        if only_type:
            # We assure that the node type that we want to retrieve
            # is the same that was indicated in the field type
            assert type == only_type._meta.name, 'Received not compatible node.'

        if type == 'User':
            return one_or_none(session=info.context.get('session'), model=User, id=global_id)
        elif type == 'Well':
            return one_or_none(session=info.context.get('session'), model=Well, id=global_id)


class ControlledVocabulary(SQLAlchemyInterface):
    class Meta:
        name = 'ControlledVocabularyNode'
        model = BaseControlledVocabulary


class TrackedEntity(SQLAlchemyInterface):
    class Meta:
        name = 'TrackedEntityNode'
        model = TrackedEntityModel

class Request(SQLAlchemyObjectType):
    """Request node."""

    class Meta:
        model = RequestModel
        interfaces = (TrackedEntity,)

class User(SQLAlchemyObjectType):
    """User Node"""

    class Meta:
        model = UserModel
        interfaces = (CustomNode,)

class CvFormFieldValueType(SQLAlchemyObjectType):
    class Meta:
        model = CvFormFieldValueTypeModel
        interfaces = (ControlledVocabulary,)


common_field_kwargs = {'id': graphene.UUID(required=False), 'label': graphene.String(required=False)}

class Query(graphene.ObjectType):
    """Query objects for GraphQL API."""

    node = CustomNode.Field()
    te_node = TrackedEntity.Field()
    cv_node = ControlledVocabulary.Field()

    # Non-Tracked Entities:
    users: List[User] = SQLAlchemyConnectionField(User)
    # Generic Query for any Tracked Entity:
    tracked_entities: List[TrackedEntity] = FilteredConnectionField(TrackedEntity, sort=None, filter=graphene.Argument(TrackedEntityInput))
    # Generic Query for any Controlled Vocabulary:
    cv: ControlledVocabulary = graphene.Field(ControlledVocabulary, controlled_vocabulary_type_id=graphene.UUID(required=False),
                                              base_entry_key=graphene.String(required=False),
                                              **common_field_kwargs)
    cvs: List[ControlledVocabulary] = FilteredConnectionField(ControlledVocabulary, sort=None, filter=graphene.Argument(CvInput))

    @staticmethod
    def resolve_with_filters(info: ResolveInfo, model: Type[SQLAlchemyObjectType], **kwargs):
        query = model.get_query(info)
        log.debug(kwargs)
        for filter_name, filter_value in kwargs.items():
            model_filter_column = getattr(model._meta.model, filter_name, None)
            log.debug(type(filter_value))
            if not model_filter_column:
                continue
            if isinstance(filter_value, SQLAlchemyInputObjectType):
                log.debug(True)
                filter_model = filter_value.sqla_model
                q = FilteredConnectionField.get_query(filter_model, info, sort=None, **kwargs)
                # noinspection PyArgumentList
                query = query.filter(model_filter_column == q.filter_by(**filter_value))
                log.info(query)
            else:
                query = query.filter(model_filter_column == filter_value)
        return query

    def resolve_tracked_entity(self, info: ResolveInfo, **kwargs):
        entity: TrackedEntity = Query.resolve_with_filters(info=info, model=BaseTrackedEntity, **kwargs).one()
        return entity

    def resolve_tracked_entities(self, info, **kwargs):
        query = Query.resolve_with_filters(info=info, model=BaseTrackedEntity, **kwargs)
        tes: List[BaseTrackedEntity] = query.all()
        return tes

    def resolve_cv(self, info, **kwargs):
        cv: List[BaseControlledVocabulary] = Query.resolve_with_filters(info=info, model=BaseControlledVocabulary, **kwargs).one()
        log.info(cv)
        return cv

    def resolve_cvs(self, info, **kwargs):
        cv: List[BaseControlledVocabulary] = Query.resolve_with_filters(info=info, model=BaseControlledVocabulary, **kwargs).all()
        return cv

我希望不能使用SQLAlchemyInterface扩展Node,而是将Node添加回TrackedEntity和ControlledVocabulary的接口列表中,但是能够执行如下查询:

schema = Schema(query=Query, types=[*tracked_members, *cv_members])

0 个答案:

没有答案