-
-
Notifications
You must be signed in to change notification settings - Fork 32k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixed #21454 -- Added readonly attribute to Model Fields #7515
Conversation
with CaptureQueriesContext(connection=connection) as capture: | ||
inst.save() | ||
|
||
query = capture.captured_queries[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no assertion on the number of queries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sur one is needed here. It's present in the other tests when we need tu make sur we're checking the right query. That being said, I can add a check if you think it's better with than without.
@@ -387,3 +387,26 @@ class UUIDChild(PrimaryKeyUUIDModel): | |||
|
|||
class UUIDGrandchild(UUIDChild): | |||
pass | |||
|
|||
############################################################################### | |||
# This model is used for Readonly test cases |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this comment is redundant given the model names. None of the other models in the file have such comments 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
|
||
@classmethod | ||
def populate_database_fields(cls): | ||
connection.cursor().execute( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a single multiline string would be clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
def populate_database_fields(cls): | ||
connection.cursor().execute( | ||
'''ALTER TABLE "model_fields_modelwithreadonlyfield" ''' | ||
'''ALTER COLUMN "readonly_int1" SET DEFAULT 1 ''') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so comfortable with this given that Django should soon have database level defaults that work across databases. It should also be not much work to make this SQL compatible with MySQL and sqlite too, and put it in a migration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SQLite cannot add a default after the table is created (to my knowledge).
Mysql is clearly something we forgot ^^
class PostgreSQLReadonlyTests(ReadonlyTestsMixin, TestCase): | ||
|
||
@classmethod | ||
def populate_database_fields(cls): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wouldn't these be neater as a migration file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering, but there's no migrations in this app yet, and I feel it's easier to understand if the code is nearby. But I could completely do it with a migration, yes.
(Except I'd have to manage different DB in the migrations file...)
@@ -834,6 +834,13 @@ def save_base(self, raw=False, force_insert=False, | |||
# Once saved, this is no longer a to-be-added instance. | |||
self._state.adding = False | |||
|
|||
# Readonly fields are computed by the Database. If there are any, | |||
# we should fetch their value. | |||
if self._meta.has_readonly_fields: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be good if there was a way to disable this extra query, e.g. in an argument to save
. For example, if a model has a generated/computed field used purely for indexing on the database level, there's no need to fetch that field back upon save()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A whole new argument to save
looks a little overkill to me. The query is a simple one that should not cost much...
Plus, at some point in the future, we might use RETURNING
so there won't be any additional query for supported backends...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about testing the cases where I try to save/update only readonly fields? I feel like Django should throw some kind of error there, as it's a nonsensical operation. That said, save()
is a no-op if update_fields
is an empty list.
@@ -703,6 +704,9 @@ def contribute_to_class(self, cls, name, private_only=False, virtual_only=NOT_PR | |||
setattr(cls, 'get_%s_display' % self.name, | |||
curry(cls._get_FIELD_display, field=self)) | |||
|
|||
if self.readonly: | |||
cls._meta.has_readonly_fields = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be better as a cached_property
on Options
that iterates all the fields and returns True if any are readonly - it would be a bit less surprising to me, as then it's defined entirely within the Options
class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, this is exactly what I proposed 4 minutes after you comment 😄. This code can be removed now as it seems unused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes, sorry !
@@ -703,6 +704,9 @@ def contribute_to_class(self, cls, name, private_only=False, virtual_only=NOT_PR | |||
setattr(cls, 'get_%s_display' % self.name, | |||
curry(cls._get_FIELD_display, field=self)) | |||
|
|||
if self.readonly: | |||
cls._meta.has_readonly_fields = True | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth having a cached readonly_fields
property instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
@@ -115,6 +115,7 @@ def __init__(self, meta, app_label=None): | |||
self.pk = None | |||
self.has_auto_field = False | |||
self.auto_field = None | |||
self.has_readonly_fields = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having a @cached_property
for read-only fields would be more appropriate in this case. You'd just have to make sure you add 'readonly_fields'
to FORWARD_PROPERTIES
to make sure the cache is busted on field addition through add_field()
.
Makes me wonder if we'd like to name these fields |
So quick update / todo list :
|
DutH 2016 sprints : Pair coded with @wengole. Made SQLCompiler remove readonly fields from INSERT and UPDATE queries. Made model instances refresh themselves after save when appropriate.
DutH 2016 sprints : Pair coded with @wengole. Made SQLCompiler remove readonly fields from INSERT and UPDATE queries. Made model instances refresh themselves after save when appropriate.
@charette, @adamchainz : we've made corrections with @wengole, we haven't squashed the commits yet so that you can have a look at the corrections, let us know when we can squash. And thank you everyone for those wonderful sprints :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea: shouldn't AutoField
and BigAutoField
technically now be readonly
?
# Readonly fields are computed by the Database. If there are any, | ||
# we should fetch their value. (refresh_from_db does nothing if the | ||
# field list is empty) | ||
self.refresh_from_db(fields=[field.name for field in self._meta.readonly_fields]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've thought about this more and think this should be disabled by default (or even removed, users can manually call refresh_from_db
after save
). I think most use cases for readonly fields, python code will not require the new value from the database immediately. Most Django workflows typically take user data and update the database, and return - not requiring the new data from the database. In addition, virtual columns will be a key use case for readonly fields, but they are mostly added just for allowing more selectivity on queries. By adding an extra query to every single save()
on any model with a readonly field, we could be introducing a major performance regression for some users, for the off chance use case that after save()
ing a model, the readonly columns' values are required. Even if it is kept, it should be flagged, so that users can avoid the performance hit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it counts as a performance regression : it would only affect users who have defined readonly fields (except if you're talking about the cost of the sole function call to refresh_from_db
that returns immediately, but I guess not). But I get your point. And even if the select should not be costly, you're right in that it's not our call to make (it might be a minor point for some and a major point for others).
In this case, we could at least provide a refresh_readonly_fields
method (or something like this). Note : this could be interesting independently, because readonly fields (or computed fields) have a higher chance to see their value change independently from what happens on the Python side.
I'm thinking this be an important design decisions here, that could be brought to the dev mailing list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative approach would be to return a lazy object for that field which runs the query when accessed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, a whole object seems overkill because it would mean one does not get the same kind of object when calling create or save() in a MySQL and PGSQL environment, but maybe we could just mark those fields as deferred, so there would be a query when one tries to access the fields (maybe with a way to load them all at once ?)
has_fields = bool(self.query.fields) | ||
fields = self.query.fields if has_fields else [opts.pk] | ||
# Filter out readonly fields | ||
fields = [field for field in self.query.fields if not field.readonly] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any case where self.query.fields
can be None
? The previous code was preventing from iterating over it in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you're right.
|
||
writable_values = [ | ||
(field, model, val) | ||
for field, model, val in self.query.values |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same remark about an eventual None
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same as above, you're right)
AutoField and BigAutoField can be written by Django. You can decide you want to set the value of the AutoField to 42. But for readonly fields, the only way to bypass it is to write your own SQL. These are close but not identical concept and while one can inspire the other, they're both different. I was also thinking about the "readonly" vs "writable" debate : readonly has the advantage of being defined as a positive attribute (the default is False and the non-default is True). The equivalent naming of |
I've raise the different issues on the Mailing list and for now, I'm enclined to follow @mjtamlyn 's suggestion of creating a DEP to make sure our solution fit all the common usecases. This means that it's likely that this PR will not be merged as is, even if it can be an inspiration for a future attempt. I'm personnally convinced that the readonly part is good and when the dust settles, and if the plan says so, I may resubmit this branch with just the readonly part. @wengole , this is your code too. Do you agree ? |
I think using deferred would makes sense here, that's exactly what these It wouldn't be too hard for a user requiring direct access to these fields 2016-11-10 7:55 GMT-05:00 Joachim Jablon notifications@github.com:
|
( @charettes was replying to #7515 (comment) but of course, GitHub doesn't show this properly) |
@ewjoachim thanks, I can review the DEP but I won't have the time to write anything myself. I have already another big feature pending for the next LTS. It eats up all the time I can spend on Django right now. |
I'll reopen a PR when it's ready. |
DutH 2016 sprints : Pair coded with @wengole.
Made SQLCompiler remove readonly fields from INSERT and UPDATE queries.
Made model instances refresh themselves after save when appropriate.
https://groups.google.com/forum/#!topic/django-developers/BDAlTyJwQeY