Love Hate Relationship With Signal
Signal. Salah satu fitur yang cukup powerfull di django, saking powerfullnya jika tidak tertib menggunakannya ini bisa menjadi bumerang, serius penggunaan signal kalau tidak hati-hati malah memperumit.
Perlu diakui penggunaan signal ini bisa memudahkan beberapa hal dalam membangun sebuah fitur, terutama fitur-fitur yang mebutuhkan sebuah proses lanjutan.
Sebelumnya mari kenalan dulu dengan signal, dikutip langsung dari dokumentasi resmi django
In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. They’re especially useful when many pieces of code may be interested in the same events.
Berdasarkan pengalaman pribadi fitur signal yang paling sering digunakan adalah pre_save
dan post_save
pre_save
: Melakukan sebuah aksi sebelum sebuah instance model disimpan.
post_save
: Melakukan sebuah aksi setelah sebuah instance model disimpan
Contoh:
pre_save
Misalkan kita ingin mengirimkan email sukses jika invoice sudah lunas tapi ketika invoice ditolak maka kita akan mengirimkan email batal.
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import Invoice
@receiver(pre_save, sender=Invoice)
def send_email_on_status_change(sender, instance, **kwargs):
# Get the original status of the invoice before saving
try:
original_invoice = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
original_invoice = None
# Check if the status has changed
if original_invoice and original_invoice.status != instance.status:
# If the status changed to "PAID"
if instance.status == 'PAID':
print("send email success")
# If the status changed to "CANCEL"
elif instance.status == 'CANCEL':
print("send email success")
Contoh di atas adalah salah satu pendekatan penggunaan signal pre_save di mana kita bisa mendeteksi transisi sebuah status dan menjalankan fungsi berdasarkan status tersebut.
post_save
from django.core.mail import send_mail
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Product
@receiver(post_save, sender=Product)
def send_product_notification(sender, instance, **kwargs):
if instance.status == 'watch':
# If the Product instance status is "watch"
# Send an email notification to the subscribed users
subscribers = instance.subscribers.all()
if subscribers:
product_name = instance.name
product_description = instance.description
message = f"Product {product_name} is now available for watching. Description: {product_description}"
for user in subscribers:
send_mail(
'Product Update',
message,
'[email protected]',
[user.email],
fail_silently=False,
)
Contoh di atas misal kita punya satu product yang sedang dipantau makanya diberi status watch
selama statusnya sedang watch dan ada proses save di model tersebut, orang yang memantau itu akan dikirimin email.
Keuntungan Menggunakan Signal
Keuntungan menggunakan signal salah satunya adalah dia bersifat global, jadi jika ada aksi yang emang bersifat global cocok sekali dipasang di signal. Misal, kita punya aplikasi yang proses mengubah data bisa datang dari banyak tempat, contoh:
- Bisa create/update dari django admin
- Bisa create/update dari template form
- Bisa create/update dari API entah REST/GraphQL
Terserah sumbernya dari mana tapi punya aksi yang sama sangat enak disimpan di signal karena darimanapun masuknya ketika modelnya melakukan save()
maka signal akan dipanggil.
Tapi hati-hati...
Sudah ada gambaran terkait signal dan ingin segera implementasi di projek kalian? Sabar, karena seperti di kalimat pembuka saya bahwa signal ini bisa jadi bumerang jika kita tidak tertib dan seenaknya.
Susah untuk di-debug
Beberapa orang termasuk saya kadang memakai signal untuk mengubah model lain, misal saat membuat order lalu quota produk akan berkurang
from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import Order, Product
@receiver(post_save, sender=Order)
def reduce_product_quota(sender, instance, created, **kwargs):
if created:
# Reduce product quota when an order is created
product = instance.product
product.quota -= instance.quantity
product.save()
Lalu tanpa sadar si product juga memiliki signal
@receiver(post_save, sender=product)
def do_something_product(sender, instance, created, **kwargs):
doing_something_with_product()
Ketika banyak signal yang seperti ini ketika terjadi sebuah issue lumayan pusing untuk di debug, karena 1 model bisa mengakibatkan perubahan di model lainnya yang masalahnya tidak explisit.
Berbeda jika misal kalau kita pasang di forms atau di views langsung
from django.shortcuts import render
from myapp.models import Order, Product
def create_order(request):
if request.method == 'POST':
# Process order form data
product_id = request.POST.get('product_id')
quantity = request.POST.get('quantity')
product = Product.objects.get(id=product_id)
product.quota -= int(quantity)
product.save()
# Create order
order = Order(product=product, quantity=quantity)
order.save()
# Render success response
return render(request, 'success.html')
else:
# Render order form
products = Product.objects.all()
return render(request, 'order.html', {'products': products})
Di sini explisit bahwa fungsi ini itu melakukan perubahan kepada model mana saja.
Race Condition
Umumnya ini terjadi di pre_save dan juga ngambil data, misal
@receiver(pre_save, sender=Model)
def race_condition(sender, instance, **kwargs):
old = Model.objects.get(id=instance.id)
if old.action != Model.PRINT and instance.action == Model.PRINT:
build_pdf.delay(instance.id)
@shared_task
def build_pdf(id):
data = Model.objects.get(ref=id)
build_data(data)
...
Kita butuh triggernya dari transisi action, tapi kode kita ternyata manggil lookup data lagi, ini yang ada ketika build data statusnya belum print jadi bisa saja yang tercetak berbeda statusnya.
Infinite Recursive
Ini juga yang paling memungkinkan,misalkita ingin menyimpan tanggal setiap berubah (ini bukan contoh yang ideal tapi ini yang akan mengakibatkan error)
@receiver(post_save, sender=Model)
def infinite_loop_signal_handler(sender, instance, **kwargs):
instance.some_field = some_value
instance.save() #
Ini akan kena error recursive karena di dalam signal dia melakukan save, manggil signal post_save lagi, terus ada save lagi, manggil post_save, terus ada save lagi, manggil post_save, terus ada...
Jadi...
Kejadian-kejadian di atas itu bukan hal yang mengada-ngada karena emang itu terjadi karena kelalaian dari penulisnya sendiri, jadi bagi saya sendiri mempunyai aturan dalam menggunakan signal seperti ini.
- Bikin aturan yang jelas terkait jenis fungsi apa saja yang perlu masuk signal
- Jika memungkinkan lebih baik ekplisit, seperti simpan di views atau di serializer
- Jika ingin ada perlakuan sebelum save atau sesudah save bisa consider replace method save modelnya jika memang fungsinya bersifat global, disimpan di sini lebih explisit.
- Pastikan peruntukannya, seperti pre save cocok untuk sanitasi data, dan post save bisa untuk mengirim notif. Hindari mengubah model baik instance sendiri atau model lain.
- Pastikan berpikir lebih baik mudah me-maintain daripada mudah membuat.