关于python:如何将SqlAlchemy结果序列化为JSON?

How to serialize SqlAlchemy result to JSON?

Django有一些从DB到JSON格式返回的ORM模型的自动序列化。

如何将SQLAlchemy查询结果序列化为JSON格式?

我尝试了jsonpickle.encode,但它对查询对象本身进行了编码。
我试过json.dumps(items)但它返回了

1
TypeError: <Product('3', 'some name', 'some desc')> is not JSON serializable

是否真的很难将SQLAlchemy ORM对象序列化为JSON / XML? 它没有默认的序列化器吗? 现在序列化ORM查询结果是非常常见的任务。

我需要的只是返回SQLAlchemy查询结果的JSON或XML数据表示。

需要在javascript datagird中使用SQLAlchemy对象的JSON / XML格式的查询结果(JQGrid http://www.trirand.com/blog/)


您可以将对象输出为dict:

1
2
3
class User:
   def as_dict(self):
       return {c.name: getattr(self, c.name) for c in self.__table__.columns}

然后使用User.as_dict()来序列化您的对象。

如将sqlalchemy行对象转换为python dict中所述


平面实施

你可以使用这样的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sqlalchemy.ext.declarative import DeclarativeMeta

class AlchemyEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj.__class__, DeclarativeMeta):
            # an SQLAlchemy class
            fields = {}
            for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
                data = obj.__getattribute__(field)
                try:
                    json.dumps(data) # this will fail on non-encodable values, like other classes
                    fields[field] = data
                except TypeError:
                    fields[field] = None
            # a json-encodable dict
            return fields

        return json.JSONEncoder.default(self, obj)

然后使用以下命令转换为JSON:

1
2
c = YourAlchemyClass()
print json.dumps(c, cls=AlchemyEncoder)

它将忽略不可编码的字段(将它们设置为"无")。

它不会自动扩展关系(因为这可能会导致自我引用,并永远循环)。

递归的,非循环的实现

但是,如果你宁愿循环,你可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from sqlalchemy.ext.declarative import DeclarativeMeta

def new_alchemy_encoder():
    _visited_objs = []

    class AlchemyEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj.__class__, DeclarativeMeta):
                # don't re-visit self
                if obj in _visited_objs:
                    return None
                _visited_objs.append(obj)

                # an SQLAlchemy class
                fields = {}
                for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
                    fields[field] = obj.__getattribute__(field)
                # a json-encodable dict
                return fields

            return json.JSONEncoder.default(self, obj)

    return AlchemyEncoder

然后使用以下方法编码对象

1
print json.dumps(e, cls=new_alchemy_encoder(), check_circular=False)

这将编码所有孩子,他们所有的孩子,以及他们所有的孩子...基本上可能编码整个数据库。当它达到之前编码的内容时,它会将其编码为"无"。

递归的,可能循环的选择性实现

另一种可能更好的替代方法是能够指定要扩展的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def new_alchemy_encoder(revisit_self = False, fields_to_expand = []):
    _visited_objs = []

    class AlchemyEncoder(json.JSONEncoder):
        def default(self, obj):
            if isinstance(obj.__class__, DeclarativeMeta):
                # don't re-visit self
                if revisit_self:
                    if obj in _visited_objs:
                        return None
                    _visited_objs.append(obj)

                # go through each field in this SQLalchemy class
                fields = {}
                for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
                    val = obj.__getattribute__(field)

                    # is this field another SQLalchemy object, or a list of SQLalchemy objects?
                    if isinstance(val.__class__, DeclarativeMeta) or (isinstance(val, list) and len(val) > 0 and isinstance(val[0].__class__, DeclarativeMeta)):
                        # unless we're expanding this field, stop here
                        if field not in fields_to_expand:
                            # not expanding this field: set it to None and continue
                            fields[field] = None
                            continue

                    fields[field] = val
                # a json-encodable dict
                return fields

            return json.JSONEncoder.default(self, obj)

    return AlchemyEncoder

你现在可以用以下方式调用它:

1
print json.dumps(e, cls=new_alchemy_encoder(False, ['parents']), check_circular=False)

例如,仅扩展名为"parents"的SQLAlchemy字段。


你可以将RowProxy转换为这样的dict:

1
 d = dict(row.items())

然后将其序列化为JSON(您必须为datetime值之类的内容指定编码器)
如果您只想要一条记录(而不是相关记录的完整层次结构),那并不难。

1
json.dumps([(dict(row.items())) for row in rs])


我建议使用最近浮出水面的图书馆棉花糖。它允许您创建序列化程序来表示模型实例,并支持关系和嵌套对象。

看看他们的SQLAlchemy示例。


Flask-JsonTools包为您的模型提供了JsonSerializableBase Base类的实现。

用法:

1
2
3
4
5
6
7
from sqlalchemy.ext.declarative import declarative_base
from flask.ext.jsontools import JsonSerializableBase

Base = declarative_base(cls=(JsonSerializableBase,))

class User(Base):
    #...

现在User模型是神奇可序列化的。

如果你的框架不是Flask,你可以抓住代码


出于安全原因,您永远不应该返回所有模型的字段。我更愿意有选择地选择它们。

Flask的json编码现在支持UUID,日期时间和关系(并为flask_sqlalchemy db.Model类添加了queryquery_class)。我更新了编码器如下:

app/json_encoder.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    from sqlalchemy.ext.declarative import DeclarativeMeta
    from flask import json


    class AlchemyEncoder(json.JSONEncoder):
        def default(self, o):
            if isinstance(o.__class__, DeclarativeMeta):
                data = {}
                fields = o.__json__() if hasattr(o, '__json__') else dir(o)
                for field in [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']]:
                    value = o.__getattribute__(field)
                    try:
                        json.dumps(value)
                        data[field] = value
                    except TypeError:
                        data[field] = None
                return data
            return json.JSONEncoder.default(self, o)

app/__init__.py

1
2
3
# json encoding
from app.json_encoder import AlchemyEncoder
app.json_encoder = AlchemyEncoder

有了这个,我可以选择添加一个__json__属性,返回我想编码的字段列表:

app/models.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Queue(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    song_id = db.Column(db.Integer, db.ForeignKey('song.id'), unique=True, nullable=False)
    song = db.relationship('Song', lazy='joined')
    type = db.Column(db.String(20), server_default=u'audio/mpeg')
    src = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, server_default=db.func.now())
    updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())

    def __init__(self, song):
        self.song = song
        self.src = song.full_path

    def __json__(self):
        return ['song', 'src', 'type', 'created_at']

我将@jsonapi添加到我的视图中,返回结果列表,然后我的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[

{

   "created_at":"Thu, 23 Jul 2015 11:36:53 GMT",
   "song":

        {
           "full_path":"/static/music/Audioslave/Audioslave [2002]/1 Cochise.mp3",
           "id": 2,
           "path_name":"Audioslave/Audioslave [2002]/1 Cochise.mp3"
        },
   "src":"/static/music/Audioslave/Audioslave [2002]/1 Cochise.mp3",
   "type":"audio/mpeg"
}

]


您可以使用SqlAlchemy的内省作为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mysql = SQLAlchemy()
from sqlalchemy import inspect

class Contacts(mysql.Model):  
    __tablename__ = 'CONTACTS'
    id = mysql.Column(mysql.Integer, primary_key=True)
    first_name = mysql.Column(mysql.String(128), nullable=False)
    last_name = mysql.Column(mysql.String(128), nullable=False)
    phone = mysql.Column(mysql.String(128), nullable=False)
    email = mysql.Column(mysql.String(128), nullable=False)
    street = mysql.Column(mysql.String(128), nullable=False)
    zip_code = mysql.Column(mysql.String(128), nullable=False)
    city = mysql.Column(mysql.String(128), nullable=False)
    def toDict(self):
        return { c.key: getattr(self, c.key) for c in inspect(self).mapper.column_attrs }

@app.route('/contacts',methods=['GET'])
def getContacts():
    contacts = Contacts.query.all()
    contactsArr = []
    for contact in contacts:
        contactsArr.append(contact.toDict())
    return jsonify(contactsArr)

@app.route('/contacts/<int:id>',methods=['GET'])
def getContact(id):
    contact = Contacts.query.get(id)
    return jsonify(contact.toDict())

从这里的答案中获得灵感:
将sqlalchemy行对象转换为python dict


更详细的解释。
在您的模型中,添加:

1
2
def as_dict(self):
       return {c.name: str(getattr(self, c.name)) for c in self.__table__.columns}

str()用于python 3,因此如果使用python 2则使用unicode()。它应该有助于反序列化日期。如果不处理那些,你可以删除它。

您现在可以像这样查询数据库

1
some_result = User.query.filter_by(id=current_user.id).first().as_dict()

需要First()才能避免出现奇怪的错误。 as_dict()现在将反序列化结果。反序列化后,它已准备好转为json

1
jsonify(some_result)

它不是那么直截了当。我写了一些代码来做到这一点。我还在努力,它使用MochiKit框架。它基本上使用代理和注册的JSON转换器在Python和Javascript之间转换复合对象。

数据库对象的浏览器端是db.js
它需要proxy.js中的基本Python代理源。

在Python端有基本代理模块。
最后是webserver.py中的SqlAlchemy对象编码器。
它还取决于models.py文件中的元数据提取器。


这是一个解决方案,可让您根据自己的意愿选择要包含在输出中的关系。
注意:这是一个完整的重写,将dict / str作为arg而不是列表。修复了一些东西......

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def deep_dict(self, relations={}):
   """Output a dict of an SA object recursing as deep as you want.

    Takes one argument, relations which is a dictionary of relations we'd
    like to pull out. The relations dict items can be a single relation
    name or deeper relation names connected by sub dicts

    Example:
        Say we have a Person object with a family relationship
            person.deep_dict(relations={'family':None})
        Say the family object has homes as a relation then we can do
            person.deep_dict(relations={'family':{'homes':None}})
            OR
            person.deep_dict(relations={'family':'homes'})
        Say homes has a relation like rooms you can do
            person.deep_dict(relations={'family':{'homes':'rooms'}})
            and so on...
   """

    mydict =  dict((c, str(a)) for c, a in
                    self.__dict__.items() if c != '_sa_instance_state')
    if not relations:
        # just return ourselves
        return mydict

    # otherwise we need to go deeper
    if not isinstance(relations, dict) and not isinstance(relations, str):
        raise Exception("relations should be a dict, it is of type {}".format(type(relations)))

    # got here so check and handle if we were passed a dict
    if isinstance(relations, dict):
        # we were passed deeper info
        for left, right in relations.items():
            myrel = getattr(self, left)
            if isinstance(myrel, list):
                mydict[left] = [rel.deep_dict(relations=right) for rel in myrel]
            else:
                mydict[left] = myrel.deep_dict(relations=right)
    # if we get here check and handle if we were passed a string
    elif isinstance(relations, str):
        # passed a single item
        myrel = getattr(self, relations)
        left = relations
        if isinstance(myrel, list):
            mydict[left] = [rel.deep_dict(relations=None)
                                 for rel in myrel]
        else:
            mydict[left] = myrel.deep_dict(relations=None)

    return mydict

所以对于一个使用人/家庭/家庭/房间的例子......把它变成json所有你需要的是

1
json.dumps(person.deep_dict(relations={'family':{'homes':'rooms'}}))


虽然最初的问题可以追溯到一段时间,但这里的答案数量(以及我自己的经验)表明,这是一个非常重要的问题,有许多不同的方法,通过不同的权衡来改变复杂性。

这就是为什么我构建了SQLAthanor库,它扩展了SQLAlchemy的声明性ORM,并提供了可配置的序列化/反序列化支持,您可能需要查看它。

该库支持:

  • Python 2.7,3.4,3.5和3.6。
  • SQLAlchemy版本0.9及更高版本
  • 与JSON,CSV,YAML和Python dict的序列化/反序列化
  • 列/属性,关系,混合属性和关联代理的序列化/反序列化
  • 为特定格式和列/关系/属性启用和禁用序列化(例如,您希望支持入站password值,但从不包含出站值)
  • 预序列化和反序列化后的值处理(用于验证或类型强制)
  • 一个非常直接的语法,它既是Pythonic又与SQLAlchemy自己的方法无缝对应

你可以在这里查看(我希望!)综合文档:https://sqlathanor.readthedocs.io/en/latest

希望这可以帮助!


自定义序列化和反序列化。

"from_json"(类方法)基于json数据构建Model对象。

"deserialize"只能在实例上调用,并将所有数据从json合并到Model实例中。

"序列化" - 递归序列化

需要__write_only__属性来定义只写属性(例如"password_hash")。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Serializable(object):
    __exclude__ = ('id',)
    __include__ = ()
    __write_only__ = ()

    @classmethod
    def from_json(cls, json, selfObj=None):
        if selfObj is None:
            self = cls()
        else:
            self = selfObj
        exclude = (cls.__exclude__ or ()) + Serializable.__exclude__
        include = cls.__include__ or ()
        if json:
            for prop, value in json.iteritems():
                # ignore all non user data, e.g. only
                if (not (prop in exclude) | (prop in include)) and isinstance(
                        getattr(cls, prop, None), QueryableAttribute):
                    setattr(self, prop, value)
        return self

    def deserialize(self, json):
        if not json:
            return None
        return self.__class__.from_json(json, selfObj=self)

    @classmethod
    def serialize_list(cls, object_list=[]):
        output = []
        for li in object_list:
            if isinstance(li, Serializable):
                output.append(li.serialize())
            else:
                output.append(li)
        return output

    def serialize(self, **kwargs):

        # init write only props
        if len(getattr(self.__class__, '__write_only__', ())) == 0:
            self.__class__.__write_only__ = ()
        dictionary = {}
        expand = kwargs.get('expand', ()) or ()
        prop = 'props'
        if expand:
            # expand all the fields
            for key in expand:
                getattr(self, key)
        iterable = self.__dict__.items()
        is_custom_property_set = False
        # include only properties passed as parameter
        if (prop in kwargs) and (kwargs.get(prop, None) is not None):
            is_custom_property_set = True
            iterable = kwargs.get(prop, None)
        # loop trough all accessible properties
        for key in iterable:
            accessor = key
            if isinstance(key, tuple):
                accessor = key[0]
            if not (accessor in self.__class__.__write_only__) and not accessor.startswith('_'):
                # force select from db to be able get relationships
                if is_custom_property_set:
                    getattr(self, accessor, None)
                if isinstance(self.__dict__.get(accessor), list):
                    dictionary[accessor] = self.__class__.serialize_list(object_list=self.__dict__.get(accessor))
                # check if those properties are read only
                elif isinstance(self.__dict__.get(accessor), Serializable):
                    dictionary[accessor] = self.__dict__.get(accessor).serialize()
                else:
                    dictionary[accessor] = self.__dict__.get(accessor)
        return dictionary

在SQLAlchemy中使用内置的序列化程序:

1
2
3
4
5
6
7
from sqlalchemy.ext.serializer import loads, dumps
obj = MyAlchemyObject()
# serialize object
serialized_obj = dumps(obj)

# deserialize object
obj = loads(serialized_obj)

如果要在会话之间传输对象,请记住使用session.expunge(obj)从当前会话中分离对象。
要再次附加它,只需执行session.add(obj)


1
2
def alc2json(row):
    return dict([(col, str(getattr(row,col))) for col in row.__table__.columns.keys()])

我以为我会用这个打高尔夫球。

仅供参考:我使用的是automap_base,因为我们根据业务需求提供了单独设计的模式。我今天刚开始使用SQLAlchemy但是文档声明automap_base是declarative_base的扩展,它似乎是SQLAlchemy ORM中的典型范例,所以我相信这应该有效。

根据Tjorriemorrie的解决方案,它不会使用以下外键,但它只是将列与值匹配并通过str()处理Python类型 - 列值。我们的值包括Python datetime.time和decimal.Decimal类类型的结果,因此它可以完成工作。

希望这可以帮助任何路人!


以下代码将sqlalchemy结果序列化为json。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
from collections import OrderedDict


def asdict(self):
    result = OrderedDict()
    for key in self.__mapper__.c.keys():
        if getattr(self, key) is not None:
            result[key] = str(getattr(self, key))
        else:
            result[key] = getattr(self, key)
    return result


def to_array(all_vendors):
    v = [ ven.asdict() for ven in all_vendors ]
    return json.dumps(v)

叫声有趣,

1
2
3
def all_products():
    all_products = Products.query.all()
    return to_array(all_products)

AlchemyEncoder很棒,但有时会失败并带有Decimal值。这是一个改进的编码器,可以解决小数问题 -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AlchemyEncoder(json.JSONEncoder):
# To serialize SQLalchemy objects
def default(self, obj):
    if isinstance(obj.__class__, DeclarativeMeta):
        model_fields = {}
        for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
            data = obj.__getattribute__(field)
            print data
            try:
                json.dumps(data)  # this will fail on non-encodable values, like other classes
                model_fields[field] = data
            except TypeError:
                model_fields[field] = None
        return model_fields
    if isinstance(obj, Decimal):
        return float(obj)
    return json.JSONEncoder.default(self, obj)

我知道这是一个相当老的帖子。我接受了@SashaB给出的解决方案并根据我的需要进行了修改。

我添加了以下内容:

  • 字段忽略列表:序列化时要忽略的字段列表
  • 字段替换列表:包含在序列化时由值替换的字段名称的字典。
  • 已删除的方法和BaseQuery被序列化
  • 我的代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    def alchemy_json_encoder(revisit_self = False, fields_to_expand = [], fields_to_ignore = [], fields_to_replace = {}):
      """
       Serialize SQLAlchemy result into JSon
       :param revisit_self: True / False
       :param fields_to_expand: Fields which are to be expanded for including their children and all
       :param fields_to_ignore: Fields to be ignored while encoding
       :param fields_to_replace: Field keys to be replaced by values assigned in dictionary
       :return: Json serialized SQLAlchemy object
      """

       _visited_objs = []
       class AlchemyEncoder(json.JSONEncoder):
          def default(self, obj):
            if isinstance(obj.__class__, DeclarativeMeta):
                # don't re-visit self
                if revisit_self:
                    if obj in _visited_objs:
                        return None
                    _visited_objs.append(obj)

                # go through each field in this SQLalchemy class
                fields = {}
                for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata' and x not in fields_to_ignore]:
                    val = obj.__getattribute__(field)
                    # is this field method defination, or an SQLalchemy object
                    if not hasattr(val,"__call__") and not isinstance(val, BaseQuery):
                        field_name = fields_to_replace[field] if field in fields_to_replace else field
                        # is this field another SQLalchemy object, or a list of SQLalchemy objects?
                        if isinstance(val.__class__, DeclarativeMeta) or \
                                (isinstance(val, list) and len(val) > 0 and isinstance(val[0].__class__, DeclarativeMeta)):
                            # unless we're expanding this field, stop here
                            if field not in fields_to_expand:
                                # not expanding this field: set it to None and continue
                                fields[field_name] = None
                                continue

                        fields[field_name] = val
                # a json-encodable dict
                return fields

            return json.JSONEncoder.default(self, obj)
       return AlchemyEncoder

    希望它可以帮到某人!


    在Flask下,这可以处理和处理数据时间字段,转换类型的字段
    'time': datetime.datetime(2018, 3, 22, 15, 40)
    "time":"2018-03-22 15:40:00"

    1
    2
    3
    4
    5
    6
    7
    obj = {c.name: str(getattr(self, c.name)) for c in self.__table__.columns}

    # This to get the JSON body
    return json.dumps(obj)

    # Or this to get a response object
    return jsonify(obj)

    带有utf-8的内置串行器扼流圈无法解码某些输入的无效起始字节。相反,我去了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    def row_to_dict(row):
        temp = row.__dict__
        temp.pop('_sa_instance_state', None)
        return temp


    def rows_to_list(rows):
        ret_rows = []
        for row in rows:
            ret_rows.append(row_to_dict(row))
        return ret_rows


    @website_blueprint.route('/api/v1/some/endpoint', methods=['GET'])
    def some_api():
        '''
        /some_endpoint
        '''

        rows = rows_to_list(SomeModel.query.all())
        response = app.response_class(
            response=jsonplus.dumps(rows),
            status=200,
            mimetype='application/json'
        )
        return response


    我利用(太多?)词典:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def serialize(_query):
        #d = dictionary written to per row
        #D = dictionary d is written to each time, then reset
        #Master = dictionary of dictionaries; the id Key (int, unique from database)
        from D is used as the Key for the dictionary D entry in Master
        Master = {}
        D = {}
        x = 0
        for u in _query:
            d = u.__dict__
            D = {}
            for n in d.keys():
               if n != '_sa_instance_state':
                        D[n] = d[n]
            x = d['id']
            Master[x] = D
        return Master

    使用flask(包括jsonify)和flask_sqlalchemy运行以打印输出为JSON。

    使用jsonify(serialize())调用该函数。

    适用于我迄今为止尝试的所有SQLAlchemy查询(运行SQLite3)