I am upgrading a Django app from 2.2.7 to 3.1.3. The app uses Postgres 12 & psycopg2 2.8.6.
I followed the instructions and changed all my django.contrib.postgres.fields.JSONField
references to django.db.models.JSONField
, and made and ran the migrations. This produced no changes to my schema (which is good.)
However, when I execute a raw query the data for those jsonb
columns is returned as text, or converted to text, at some point. I don't see this issue when querying the models directly using Model.objects.get(...)
.
import os, django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "big_old_project.settings")
django.setup()
with connection.cursor() as c:
c.execute("select name, data from tbl where name=%s", ("rex",))
print(c.description)
for row in c.fetchall():
for col in row:
print(f"{type(col)} => {col!r}")
(Column(name='name', type_code=1043), Column(name='data', type_code=3802))
<class 'str'> => 'rex'
<class 'str'> => '{"toy": "bone"}'
[edit] Using a raw connection gives the expected results:
conn = psycopg2.connect("dbname=db user=x password=z")
with conn.cursor() as c:
...
<class 'str'> => 'rex'
<class 'dict'> => {'toy': 'bone'}
Trying the old trick of "registering" the adapter doesn't work, and shouldn't be needed anyway.
import psycopg2.extras
psycopg2.extras.register_json(oid=3802, array_oid=3807, globally=True)
This app has a lot of history, so maybe something is stepping on psycopg2's toes? I can't find anything so far, and have commented out everything that seems tangentially related.
Going through the release notes didn't help. I do use other postgres fields so I can't delete all references to contrib.postgres.fields
from my models.
Any ideas as to why this is happening would be greatly appreciated.
Querying JSON dataPostgreSQL provides two native operators -> and ->> to help you query JSON data. The operator -> returns JSON object field by key. The operator ->> returns JSON object field by text.
To query data, you can use a SELECT statement like in any other SQL query. You can use the native PostgreSQL operators to query the data in PostgreSQL. The operator -> returns a JSON object field by key. The operator ->> returns a JSON object field by text.
JSON stands for JavaScript Object Notation. It is used to store data in the form of key-value pairs and is generally used for communicating between the server and the client. Contrary to other formats, JSON is human-readable text. PostgreSQL has support for native JSON data type since version 9.2.
The json data type stores an exact copy of the input text, which processing functions must reparse on each execution; while jsonb data is stored in a decomposed binary format that makes it slightly slower to input due to added conversion overhead, but significantly faster to process, since no reparsing is needed.
To add to @Andrew Backer's helpful answer, this is apparently intentional. From the 3.1.1 release notes:
Fixed a
QuerySet.order_by()
crash on PostgreSQL when ordering and grouping byJSONField
with a customdecoder
(#31956). As a consequence, fetching aJSONField
with raw SQL now returns a string instead of pre-loaded data. You will need to explicitly calljson.loads()
in such cases.
It's surprising to find an API-incompatible change as an aside in a bugfix release. For now I'll be adding json.loads()
calls since, as already mentioned, there's no guarantee the ::json
workaround doesn't break as well!
Ok, so this seems to be a changed they introduced for some reason in django 3.1.1 to fix some other bug. It de-registers the jsonb
converter from the underlying connection, which IMO is terrible.
Django issues: The first "fix" broke this basic functionality along with a denial of the use case of raw sql, the second is declaring the breakage invalid.
To fix this you can either make your own raw cursor, which django doesn't screw with, or cast your fields in the raw sql. At least, that is, until they break that too!
SELECT
data::json, -- db type is jsonb
other_fields
FROM
table
Thank you for this helpful post! My solution for this bug looks like this:
def call_ux_database(query, vars=None):
conn = connections[settings.UX_DATABASE_NAME]
conn.ensure_connection()
with conn.connection.cursor() as cursor:
psycopg2.extras.register_default_jsonb(conn_or_curs=cursor)
cursor.execute(query, vars)
row = cursor.fetchall()
return row
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With