Обычно вообще не пишу комментарии, оставляю лайки и т.д. В общем хотел бы поделиться своим личным наблюдением. Уже где-то около годика учу питон, джангу и все что с этого вытекает. Пересмотрел просто огромное количество курсов как платных так и бесплатных, книжек, статей. Пока что из всего что я видел твой курс, однозначно, лучший (как этот, так и предыдущий). Во всех же остальных курсах, как правило, просто освещают какие-то базовые аспекты, которые в доке можно нарыть за 1 день, если не парочку часов) Я и раньше натыкался на твой, канал, когда еще мало в чем понимал и меня отпугнуло, на тот момент, количество просмотров и твоя подача. Сейчас заметил тенденцию, что как раз каналы на которых очень мало просмотров ( по меркам современного ютуба ) дают наиболее качественную инфу. Ну в каком-то роде я даже рад этому, меньше людей знают про такие качественные каналы, меньше конкурентов будет среди джунов (а их сейчас ой как не мало), с другой стороны, желаю тебе больше просмотров, как качественному контентмейкеру)
Спасибо за такой хороший отзыв! Да, я никак не раскручиваю канал и на нем не зарабатываю . Я понимаю почему большие каналы часто делают контент хуже. У меня есть несколько любимых (не IT) каналов , иногда смотрю видео и думаю, типа «высосано из пальца», вообще можно было не снимать, идей почти ноль, а потом рекламная интеграция. Типа делают хоть что-то , чтобы рекламу отбить. Сложно делать регулярно видео и оставаться на хорошем уровне.
Спасибо, ценная информация! Очень нравится, что все делается не спеша и хорошо объясняется. В большинство видео, которое видел, обычно все рассказывается быстро и не все сразу можно уловить. Тут же создается специально проблема, которую решают и при это рассказывают, как делать нужно, как не нужно, зачем то, зачем это. Респект!
Когда меняешь Docker-compose Ребилд делать не нужно, а просто перезапустить. Ты когда пишешь docker-compose up, он понимает, какой именно ты файл запускаешь. То, что ты пишешь в docker-compose.yml - это просто настройки. Билдить нужно только тогда, когда ты добавляешь новые библиотеки в проект или новые сервисы.
С синглтоном немного непонятен смысл. Получается, менеджер действительно ошибся, указав не то значение, но успел нажать Save. Потом тут же поменял значение, снова сохранил и довольный жизнью закрыл админку. А под капотом в реальности цена подписок не меняется, хотя у самого Service цена изменилась.
Это может быть и два менеджера, которые одновременно редактируют объект. А вообще , таски запускаются асинхронно и та, которую поставили в очередь позже, может вроде запуститься раньше. И с этим тоже приходится жить )
Сказать, что автор - молодец, значит ничего не сказать! Огромное спасибо за материал! Однако же, вставлю свои 5 копеек (исправьте, где не прав, плз): 1) "метод save() возвращает None, поэтому return не нужен." Я ОШИБСЯ. Извините. Возвращает. Странно, в доке по Джанго везде вызывают родительский save() без return. 2) Сперва нужно вызвать super().save(), а потом только запускать таски. Иначе таски дергают старый plan.discount_percent 3) А нафига же сначала запускать цикл по self.subscriptions.all()? Получается, цикл вызывает запрос просто чтобы получить айдишники (тогда хотя бы получать только айдишники с помощью "self.subscriptions.values_list('id', flat=True)"), а потом еще и таски на каждый полученный айдишник дергают из БД subscription по этому айдишнику! Не лучше ли переписать таску, чтобы она получала только айди самого экземпляра Plan, а уже внутри получала запись Plan из БД, подтягивала под него кверисет и работала напрямую с ним? 3.1) В таком случае в таске не надо будет циклом обращаться к "subscription.plan.discount_percent", ведь мы его один раз возьмем из экземпляра Plan. 3.2) Кверисет для самой таски тоже бы оптимизировать, например ".subscriptions.select_related('service').only('service__full_price', 'plan_id')". Хотя, если по совести, то к service__full_price тоже обращаться смысла нет: он же не изменился. Так что даже проще - включаем математику и всего лишь изменяем прайс сабскрипшна на столько, на сколько изменился скидочный процент )) 3.3) Внутри таски - вместо сохранения в цикле каждой записи subscription бахнем Subscription.objects.bulk_update(subscription_list, fields=['price'])! Ибо нефиг! 4) Насколько мне известно, Джанго не рекомендует ковыряться в __init__(). Вместо этого можно влезть в метод "from_db": @classmethod def from_db(cls, db, field_names, values): instance = super().from_db(db, field_names, values) instance._discount_percent = instance.discount_percent return instance def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.discount_percent != getattr(self, '_discount_percent', None): set_price.delay(self.id)
Спасибо за такой обширный комментарий. К сожалению, сейчас нет времени чтобы ответить по каждому пункту . Согласен, что не все там сделано идеально. Многие вещи специально упрощены, на какие-то просто не хватило времени чтобы додумать. В следующих курсах постараюсь учесть больше нюансов
@@Fr3PO4 Хз, у меня брало старое значение. Возможно, какие-то различия в запуске самой таски. Но, по всей здравой логике, если мы не вызвали save от родителя, то мы не сохранили значение в БД. А таска при выполнении обращается к тому самому НЕсохраненному плану. Видимо, если таска запускается с опозданием (sleep), то план, конечно, успевает сохраняться.
@@SeniorPomidorDeveloper Так это специально отведенный функционал для реализации каких либо событий произошедших в системе, таким образом не придется ни чего ломать и перегружать. Например на этапе сохранения или обновление, описаны готовые события, таким образом все taski (что будут выполняться при создании или изменении) будут лежать в одном месте, их не придется размазывать по всему проекту в modal или views.
Это один из вариантов архитектуры. Мне не нравится сигналы за их неявность. Это немного не по питонски. Вызовы происходят синхронно, а в коде их не видно. Также сам смысл пересчета по сохранению мне не нравится , надо привязывать их к изменениям конкретных полей, чтобы не запускать их впустую. На сигналах такое не получится реализовать, то решение, которое нашел , делает это через дополнительный запрос в базу. Если говорить про способ переопределения , то он ничего не ломает. Во многом разработка на DRF происходит способом переопределения, ничего плохого не вижу.
@@SeniorPomidorDeveloper зато в реализации с сигналами не будет ошибки, которую встретил я. У меня воркер запускается раньше, чем база отрабатывает сохранение данных. По итогу если убрать time.sleep(5), то для расчёта используются старые данные. Не знаю, почему у вас на видео всё хорошо, но у меня воркер отрабатывает слишком быстро. Не спасает даже перенос функции save().super() выше save_price.delay()🥲 Может, я что-то сделал не так, но пока что у меня вызывает недоумение подобное использование Celery UPD: Не знаю, можно ли оставлять ссылки под видео, поэтому просто скажу, что есть целая отдельная статья, посвящённая асинхронному доступу к БД. Там предлагается использовать функцию "django.db.transaction.on_commit" Применимо к данной ситуации функцию я переписал следующим образом: def save(self, *args, **kwargs): result = super().save(*args, **kwargs) if self.__full_price != self.full_price: transaction.on_commit(lambda: [set_price.delay(subscription.id) for subscription in self.subscriptions.all()]) Хотя, возможно, корректнее было бы сделать две отдельных таски для изменения плана и изменение сервиса, чтоб не вызывать кучу функций внутри лямбда-функции
@@esofdes Да, "on_commit" использовать надо, для того чтобы таска запустилась не в процессе save() а по завершению коммита в базу. Если я этого не добавил в курс, то это моя ошибка, забыл видимо. На работе мы это используем постоянно. Что касается сигналов, то они наверное тоже решают проблему, так-как запускаются после коммита в базу. Но это не делает их использлвание обязательным. Сигналы я вроде тоже в курсе использую для on_delete, но вцелом мне не нравится это архитектурное решение, я писал выше - почему.
Очень бы хотелось один момент добавить по оптимизации - поскольку мы обновляем только одно поле в subscription, то нам необязательно писать просто save(), да и чаще всего я сам прошу коллег указывать явно поля, поэтому для явности и облегчения запроса лучше сделать subscription.save(update_fields=['price']). И ещё один момент, который сейчас пришёл в голову - для подобных тасок, которые работают с множеством инстансов модели - может быть, есть смысл попробовать bulk_update?
Да, я вроде рассказывал про update fields, наверное в других видео. Bulk update имеет смысл чтобы не делать save в интерации, но незнаю насколько он легче/быстрее делает апдейт, надо какое-то сравнительное тестирование сделать или почитать о нем. Но если апдейт просто одним запросом по фильтру , так конечно лучше
@@SeniorPomidorDeveloper чатбот сказал, что на больших данных bulk_update обещает быть быстрее, более детально я не влезал ещё) В целом спасибо за курс, просто отлично рассказываешь и по делу, нравится!
@user-gm8gh2gp2v по сравнению с save, наверное да. Но это на моей практике, редкий кейс. Если надо один объект сохранять то достаточно save, если группу по признаку, тут нужен filter().update() как один запрос. Где-нибудь в парсерах , наверное будет актуален bulk create и bulk update, мне редко приходится их писать
Огромное спасибо за труд, очень нравится подача материала. Но вот есть пару моментов: 1) раз мы запомнили старый price, не легче использовать сигналы? Там и старые данные имеется 2) мы получали список подписок и использовали только id, может стоит из БД вытаскивать только id 3) мы через цикл создавали таски для каждой подписки. Может в таск передать список id подписок. 4) в таске если будем принимать список id, можно через фильтр получить нужные подписки и вызвать update после аннотации. . update(price=F(‘annotated_price’) Можно так сделать ?
Спасибо! 1) не очень люблю сигналы. Они работают синхронно , но не очевидно, когда их много то сложно отследить что откуда вызывается , сложно дебажить . Проще на save повесить или в mixin. Там в следующих видео я буду использовать один , но без него там было не обойтись . 2) думаю это хорошо, если так можно 3) считаю что это плохая практика. От количества данных могут быть проблемы с памятью . Типа если в аргумент передать список длинной в 100.000 id. 4) про annotate().update() идея хорошая
3) в нашем случае, можно переделать таску под план и передать только id плана и в таске через annotate и update одним запросом решить. Так у нас не будет 100,000 задач
У меня вопрос. А можно было бы не переписывать save-методы для моделей, а создать signals, которые бы срабатывали при изменении полей full_price(модель service) и discount_percent(модель Plan). Или лучше все таки переписывать save для моделей?
Сигналы не срабатывают на конкретных полях, только на сохранение и удаление модели . В остальном не вижу разницы save и сигналов , вопрос организации кода в проекте.
@@SeniorPomidorDeveloper вот как! Спасибо, я точно знаю, что сигналы срабатывают на изменение поля many-to-many-field и почему-то подумал, что на изменение каждого поля можно применять сигналы. В какой-то книге читал, там автор советует оставлять модели максимально простыми и не нагружать их методы, а все выносить в сигналы. Иначе потом, когда проект становится большим, их трудно воспринимать на глаз.
У меня еще один вопрос. Допустим, на сайте мы даем возможность пользователю самостоятельно загружать видео из своего ПК. И это тяжелое задание, которое лучше бы отправить в celery, чтобы человек не ждал, когда видео загрузится, а сразу перешел на нужную нам страницу. Для этого у нас в модели есть поле FileField. Как в этом случае поступить? Можно просто добавить декоратор @shared_task к методу save( ) нашей модели? Или нам нужно переопределить save-метод, вычленив загрузку видео в отдельную функцию и отправив непосредственно ее в celery а не весь save? Буду очень признателен за подсказку.
Хм.. думаю что в этом случае никак. Мы можем вынести в таску то, что как бы уже есть у нас в приложении или то , к чему из приложения мы имеем доступ. Если пользователь делает upload ему ни закрывать вкладку , ни прерывать соединение с сайтом, все это происходит синхронно. Есть другие варианты это оптимизировать, но это не при помощи celery
Привет. А есть ли смысл при запросе из вьюхи SubscriptionView с перефетчем заюзать select_related по нужным моделям? Как-нибудь так: Subscription.objects.select_related('plan', 'service', 'client') ... bla-bla bla. А потом в питоне уже посчитать total price курсов. Там, вроде, всего один большой запрос получится. Или я что-то не так понимаю?)
Можно. Но один большой запрос (с кучей джойнов) может быть намного тяжелее нескольких маленьких. Нет универсального правильного решения, смотря как данные устроены и какое их количество
Не совсем понимаю, делал, как у Вас, но в конце вместо одной таски все равно две остаются почему-то, если быстро менять скидку тарифного плана. У меня только одна подписка с таким планом, не понимаю в чем дело
стоит наверно сначала вызвать save моделек, а потом запускать селери воркер, иначе он в теории может отработать на старых данных. result = super().save(*args, **kwargs) start_task.delay(self.blabla.id) return result
Ну там между delay и save думаю разница будет сотые секунды. Не запустится таск раньше. Но я согласен с ходом мысли. Действительно, логично было бы сделать запуск таска после.
А как так получается, что если в самом начале таски поставить задержку в 10 сек, и посмотреть изменения на сайте, то данные всё равно обновляются, не смотря на то что таска ещё активна? она прост игнорирует time spleep и идет дальше по коду ?
@@SeniorPomidorDeveloper но если мы поставим sleep(5) ближе к концу таски (после annotate), то у нас будут подобные ошибки. Есть ли способ отменять таски в очереди и оставлять последнюю?
Таска , которую уже забрал воркер, , которая уже запущена, отменяться не будет. Это не безопасно, не нужно прерывать функцию, во время выполнения. Таску , которая стоит в очереди, отменять нет смысла, в случае, когда приходит такая-же, с аналогичными аргументами. Их, по факту, еще нет. Это только записи в очереди
мне тоже показалось что тут есть проблема, но на самом деле её нет (вроде бы), попробую объяснить: - данные с которыми таска работает берутся из базы в момент выполнения таски (поэтому они всегда актуальные) - с синглтоном мы не сможем поставить в очередь таску на пересчет стоимости подписки, если такая же таска уже лежит в очереди (в этом нет смысла, наша цель обновить цену в подписке, и задача уже в очереди, благодаря первому пункту гарантируется, что цена расчитается по самым свежим данным) - при этом если такая же таска находится в процессе выполнения или уже выполнилась, то запрет на добавление в очередь не должен срабатывать Есть тонкость насчет запрещать/не запрещать добавление в очередь если аналогичная таска на выполнении. Я бы сказал, что нужно НЕ запрещать, потому что таска уже могла считать старые данные до того, как мы их изменили => чтобы гарантировать что цена подписки будет пересчитана по свежим данным нужно: - запрещать постановку в очередь если аналогичная таска лежит в очереди - разрешать постановку в очередь если аналогичная таска выполняется либо выполнена (уже забрана из очереди) Вообще, мне показалось (из того, что я увидел, сам еще не попробовал), что Singletone не ставит таску в очередь если аналогичная на выполнении. Если это так то проблема актуально. Но это было бы странно, все-таки такие вещи ведь обычно умные люди делают?))
Хороший вопрос. Документация гласит - «The Singleton class overrides apply_async() of the base task implementation to only queue a task if an identical task is not already running.» «… чтобы поставить в очередь таск, только в том случае , если аналогичный таск еще не запущен». Лично мне это формулировка совсем не понятна. То есть видимо он не будет поставлен в очередь, если таск уже запущен? А если таск аналогичный в очереди , тоже получается не будет запущен , но документация об этом не пишет. Вообщем можно взять и проверить , если у вас курс все еще есть на компе, я уже удалил. По логике я согласен с вашими мыслями, таску скорее надо ставить в очередь, если аналогичная отрабатывает , по крайней мере в кейсе с пересчетами .
Значит что-то упустили , или недосмотрели до конца. Там есть такой вариант чтобы таски по кругу друг друга вызывали и делали бесконечный save, я как раз этого избегаю в коде
@@user-lt4nw7cz8l нет, это про другое . Думаю что можно попробовать сравнить свой код с тем, что в репозитории github.com/chepe4pi/service_app в нужной ветке. Ну или пройти урок заново