Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Manajemen Transaksi dengan Django 1.6

Jika Anda pernah mencurahkan banyak waktu untuk manajemen transaksi basis data Django, Anda tahu betapa membingungkannya hal itu. Di masa lalu, dokumentasi memberikan sedikit kedalaman, tetapi pemahaman hanya datang melalui membangun dan bereksperimen.

Ada banyak dekorator untuk dikerjakan, seperti commit_on_success , commit_manually , commit_unless_managed , rollback_unless_managed , enter_transaction_management , leave_transaction_management , hanya untuk beberapa nama. Untungnya, dengan Django 1.6 itu semua keluar dari pintu. Anda benar-benar hanya perlu tahu tentang beberapa fungsi sekarang. Dan kita akan mendapatkannya hanya dalam sedetik. Pertama, kita akan membahas topik ini:

  • Apa itu manajemen transaksi?
  • Apa yang salah dengan manajemen transaksi sebelum Django 1.6?

Sebelum melompat ke:

  • Apa yang benar tentang manajemen transaksi di Django 1.6?

Dan kemudian berurusan dengan contoh terperinci:

  • Contoh Garis
  • Transaksi
  • Cara yang disarankan
  • Menggunakan dekorator
  • Transaksi per Permintaan HTTP
  • SavePoints
  • Transaksi Bertingkat

Apa itu transaksi?

Menurut SQL-92, "Transaksi SQL (kadang-kadang hanya disebut "transaksi") adalah urutan eksekusi pernyataan SQL yang bersifat atomik sehubungan dengan pemulihan". Dengan kata lain, semua pernyataan SQL dieksekusi dan dikomit bersama. Demikian juga, ketika digulung kembali, semua pernyataan digulung kembali.

Misalnya:

# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT

Jadi transaksi adalah satu unit kerja dalam database. Dan satu unit kerja itu dibatasi oleh transaksi awal dan kemudian komit atau rollback eksplisit.



Apa yang salah dengan manajemen transaksi sebelum Django 1.6?

Untuk menjawab pertanyaan ini sepenuhnya, kita harus membahas bagaimana transaksi ditangani dalam basis data, pustaka klien, dan di dalam Django.


Database

Setiap pernyataan dalam database harus dijalankan dalam suatu transaksi, meskipun transaksi tersebut hanya mencakup satu pernyataan.

Sebagian besar database memiliki AUTOCOMMIT pengaturan, yang biasanya disetel ke True sebagai default. AUTOCOMMIT . ini membungkus setiap pernyataan dalam transaksi yang segera dilakukan jika pernyataan berhasil. Tentu saja Anda dapat memanggil sesuatu seperti START_TRANSACTION . secara manual yang akan menangguhkan sementara AUTOCOMMIT sampai Anda menelepon COMMIT_TRANSACTION atau ROLLBACK .

Namun, kesimpulannya di sini adalah AUTOCOMMIT pengaturan menerapkan komit implisit setelah setiap pernyataan .



Perpustakaan Klien

Lalu ada pustaka klien Python seperti sqlite3 dan mysqldb, yang memungkinkan program Python untuk berinteraksi dengan database itu sendiri. Pustaka semacam itu mengikuti serangkaian standar tentang cara mengakses dan mengkueri database. Standar tersebut, DB API 2.0, dijelaskan dalam PEP 249. Meskipun mungkin membuat beberapa pembacaan agak kering, hal penting yang perlu diperhatikan adalah bahwa PEP 249 menyatakan bahwa database AUTOCOMMIT harus MATI secara default.

Ini jelas bertentangan dengan apa yang terjadi di dalam database:

  • Pernyataan SQL selalu harus dijalankan dalam transaksi, yang biasanya dibuka oleh database untuk Anda melalui AUTOCOMMIT .
  • Namun, menurut PEP 249, hal ini tidak boleh terjadi.
  • Library klien harus mencerminkan apa yang terjadi di dalam database, tetapi karena mereka tidak diizinkan untuk mengubah AUTOCOMMIT aktif secara default, mereka hanya membungkus pernyataan SQL Anda dalam sebuah transaksi, seperti database.

Oke. Tetap bersamaku sedikit lebih lama.



Django

Masukkan Django. Django juga memiliki sesuatu untuk dikatakan tentang manajemen transaksi. Di Django 1.5 dan sebelumnya, Django pada dasarnya menjalankan dengan transaksi terbuka dan melakukan transaksi itu secara otomatis saat Anda menulis data ke database. Jadi setiap kali Anda memanggil sesuatu seperti model.save() atau model.update() , Django menghasilkan pernyataan SQL yang sesuai dan melakukan transaksi.

Juga di Django 1.5 dan sebelumnya, disarankan agar Anda menggunakan TransactionMiddleware untuk mengikat transaksi ke permintaan HTTP. Setiap permintaan diberi transaksi. Jika respons dikembalikan tanpa pengecualian, Django akan melakukan transaksi tetapi jika fungsi tampilan Anda menimbulkan kesalahan, ROLLBACK akan dipanggil. Akibatnya, matikan AUTOCOMMIT . Jika Anda menginginkan manajemen transaksi gaya komit otomatis tingkat basis data, Anda harus mengelola sendiri transaksi - biasanya dengan menggunakan penghias transaksi pada fungsi tampilan Anda seperti @transaction.commit_manually , atau @transaction.commit_on_success .

Mengambil napas. Atau dua.



Apa artinya ini?

Ya, ada banyak hal yang terjadi di sana, dan ternyata sebagian besar pengembang hanya menginginkan autocommit tingkat basis data standar - artinya transaksi tetap berada di belakang layar, melakukan tugasnya, hingga Anda perlu menyesuaikannya secara manual.




Apa yang benar tentang manajemen transaksi di Django 1.6?

Sekarang, selamat datang di Django 1.6. Lakukan yang terbaik untuk melupakan semua yang baru saja kita bicarakan dan cukup ingat bahwa di Django 1.6, Anda menggunakan basis data AUTOCOMMIT dan mengelola transaksi secara manual bila diperlukan. Pada dasarnya, kami memiliki model yang jauh lebih sederhana yang pada dasarnya melakukan apa yang awalnya dirancang untuk dilakukan oleh database.

Cukup teori. Ayo kode.



Contoh Garis

Di sini kita memiliki contoh fungsi tampilan yang menangani pendaftaran pengguna dan memanggil Stripe untuk pemrosesan kartu kredit.

def register(request):
    user = None
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():

            customer = Customer.create("subscription",
              email = form.cleaned_data['email'],
              description = form.cleaned_data['name'],
              card = form.cleaned_data['stripe_token'],
              plan="gold",
            )

            cd = form.cleaned_data
            try:
                user = User.create(cd['name'], cd['email'], cd['password'],
                   cd['last_4_digits'])

                if customer:
                    user.stripe_id = customer.id
                    user.save()
                else:
                    UnpaidUsers(email=cd['email']).save()

            except IntegrityError:
                form.addError(cd['email'] + ' is already a member')
            else:
                request.session['user'] = user.pk
                return HttpResponseRedirect('/')

    else:
      form = UserForm()

    return render_to_response(
        'register.html',
        {
          'form': form,
          'months': range(1, 12),
          'publishable': settings.STRIPE_PUBLISHABLE,
          'soon': soon(),
          'user': user,
          'years': range(2011, 2036),
        },
        context_instance=RequestContext(request)
    )

Tampilan ini pertama kali memanggil Customer.create yang sebenarnya memanggil Stripe untuk menangani pemrosesan kartu kredit. Kemudian kita membuat pengguna baru. Jika kami mendapat tanggapan kembali dari Stripe, kami memperbarui pelanggan yang baru dibuat dengan stripe_id . Jika kami tidak mendapatkan pelanggan kembali (Stripe tidak aktif), kami akan menambahkan entri ke UnpaidUsers tabel dengan email pelanggan yang baru dibuat, sehingga kami dapat meminta mereka untuk mencoba kembali detail kartu kredit mereka nanti.

Idenya adalah bahwa meskipun Stripe tidak aktif, pengguna masih dapat mendaftar dan mulai menggunakan situs kami. Kami hanya akan meminta mereka lagi di kemudian hari untuk info kartu kredit.

Saya mengerti ini mungkin sedikit contoh yang dibuat-buat, dan ini bukan cara saya menerapkan fungsi tersebut jika harus, tetapi tujuannya adalah untuk mendemonstrasikan transaksi.

Maju. Memikirkan tentang transaksi, dan mengingat bahwa secara default Django 1.6 memberi kita AUTOCOMMIT perilaku untuk database kita, mari kita lihat kode terkait database sedikit lebih lama.

cd = form.cleaned_data
try:
    user = User.create(
        cd['name'], cd['email'], 
        cd['password'], cd['last_4_digits'])

    if customer:
        user.stripe_id = customer.id
        user.save()
    else:
        UnpaidUsers(email=cd['email']).save()

except IntegrityError:
    # ...

Dapatkah Anda menemukan masalah? Nah apa jadinya jika UnpaidUsers(email=cd['email']).save() saluran gagal?

Anda akan memiliki pengguna yang terdaftar di sistem, yang menurut sistem telah memverifikasi kartu kredit mereka, tetapi pada kenyataannya mereka belum memverifikasi kartu.

Kami hanya menginginkan satu dari dua hasil:

  1. Pengguna dibuat (dalam database) dan memiliki stripe_id .
  2. Pengguna dibuat (dalam database) dan tidak memiliki stripe_id DAN baris terkait di UnpaidUsers tabel dengan alamat email yang sama dibuat.

Yang berarti kita ingin dua pernyataan database terpisah melakukan keduanya atau keduanya rollback. Kasing yang sempurna untuk transaksi sederhana.

Pertama, mari kita tulis beberapa pengujian untuk memverifikasi bahwa segala sesuatunya berperilaku seperti yang kita inginkan.

@mock.patch('payments.models.UnpaidUsers.save', side_effect = IntegrityError)
def test_registering_user_when_strip_is_down_all_or_nothing(self, save_mock):

    #create the request used to test the view
    self.request.session = {}
    self.request.method='POST'
    self.request.POST = {'email' : '[email protected]',
                         'name' : 'pyRock',
                         'stripe_token' : '...',
                         'last_4_digits' : '4242',
                         'password' : 'bad_password',
                         'ver_password' : 'bad_password',
                        }

    #mock out stripe  and ask it to throw a connection error
    with mock.patch('stripe.Customer.create', side_effect =
                    socket.error("can't connect to stripe")) as stripe_mock:

        #run the test
        resp = register(self.request)

        #assert there is no record in the database without stripe id.
        users = User.objects.filter(email="[email protected]")
        self.assertEquals(len(users), 0)

        #check the associated table also didn't get updated
        unpaid = UnpaidUsers.objects.filter(email="[email protected]")
        self.assertEquals(len(unpaid), 0)

Dekorator di bagian atas pengujian adalah tiruan yang akan memunculkan 'IntegrityError' ketika kami mencoba menyimpan ke UnpaidUsers tabel.

Ini untuk menjawab pertanyaan, “Apa yang terjadi jika UnpaidUsers(email=cd['email']).save() garis gagal?” Bit kode berikutnya hanya membuat sesi tiruan, dengan info yang sesuai yang kita butuhkan untuk fungsi pendaftaran kita. Dan kemudian with mock.patch memaksa sistem untuk percaya bahwa Stripe sedang down ... akhirnya kita sampai pada ujian.

resp = register(self.request)

Baris di atas hanya memanggil fungsi tampilan register kita yang meneruskan permintaan yang diolok-olok. Kemudian kami hanya memeriksa untuk memastikan tabel tidak diperbarui:

#assert there is no record in the database without stripe_id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)

#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)

Jadi seharusnya gagal jika kita menjalankan tes:

======================================================================
FAIL: test_registering_user_when_strip_is_down_all_or_nothing (tests.payments.testViews.RegisterPageTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/j1z0/.virtualenvs/django_1.6/lib/python2.7/site-packages/mock.py", line 1201, in patched
    return func(*args, **keywargs)
  File "/Users/j1z0/Code/RealPython/mvp_for_Adv_Python_Web_Book/tests/payments/testViews.py", line 266, in test_registering_user_when_strip_is_down_all_or_nothing
    self.assertEquals(len(users), 0)
AssertionError: 1 != 0

----------------------------------------------------------------------

Bagus. Tampaknya lucu untuk dikatakan tetapi itulah yang kami inginkan. Ingat:kami berlatih TDD di sini. Pesan kesalahan memberi tahu kami bahwa Pengguna memang disimpan dalam database - yang sebenarnya tidak kami inginkan karena mereka tidak membayar!

Transaksi untuk menyelamatkan ...



Transaksi

Sebenarnya ada beberapa cara untuk membuat transaksi di Django 1.6.

Mari kita bahas beberapa.


Cara yang disarankan

Menurut dokumentasi Django 1.6:

“Django menyediakan satu API untuk mengontrol transaksi basis data. […] Atomicity adalah properti yang mendefinisikan transaksi database. atom memungkinkan kita untuk membuat blok kode di mana atom pada database dijamin. Jika blok kode berhasil diselesaikan, perubahan akan dilakukan ke database. Jika ada pengecualian, perubahan akan dibatalkan.”

Atomic dapat digunakan sebagai dekorator atau sebagai context_manager. Jadi jika kita menggunakannya sebagai pengelola konteks, kode dalam fungsi register kita akan terlihat seperti ini:

from django.db import transaction

try:
    with transaction.atomic():
        user = User.create(
            cd['name'], cd['email'], 
            cd['password'], cd['last_4_digits'])

        if customer:
            user.stripe_id = customer.id
            user.save()
        else:
            UnpaidUsers(email=cd['email']).save()

except IntegrityError:
    form.addError(cd['email'] + ' is already a member')

Perhatikan baris with transaction.atomic() . Semua kode di dalam blok itu akan dieksekusi di dalam transaksi. Jadi jika kami menjalankan kembali pengujian kami, semuanya harus lulus! Ingat transaksi adalah satu unit kerja, jadi semua yang ada di dalam pengelola konteks akan digulung kembali saat UnpaidUsers panggilan gagal.



Menggunakan dekorator

Kami juga dapat mencoba menambahkan atom sebagai dekorator.

@transaction.atomic():
def register(request):
    # ...snip....

    try:
        user = User.create(
            cd['name'], cd['email'], 
            cd['password'], cd['last_4_digits'])

        if customer:
            user.stripe_id = customer.id
            user.save()
        else:
                UnpaidUsers(email=cd['email']).save()

    except IntegrityError:
        form.addError(cd['email'] + ' is already a member')

Jika kami menjalankan kembali pengujian kami, mereka akan gagal dengan kesalahan yang sama seperti sebelumnya.

Mengapa demikian? Mengapa transaksi tidak dikembalikan dengan benar? Alasannya karena transaction.atomic sedang mencari semacam Pengecualian dan yah, kami menangkap kesalahan itu (yaitu IntegrityError di try kami kecuali blok), jadi transaction.atomic tidak pernah melihatnya dan dengan demikian standar AUTOCOMMIT fungsi mengambil alih.

Tapi tentu saja menghapus percobaan kecuali akan menyebabkan pengecualian hanya dilemparkan ke rantai panggilan dan kemungkinan besar meledak di tempat lain. Jadi kami juga tidak bisa melakukannya.

Jadi triknya adalah menempatkan manajer konteks atom di dalam blok try kecuali yang kami lakukan dalam solusi pertama kami. Melihat kode yang benar lagi:

from django.db import transaction

try:
    with transaction.atomic():
        user = User.create(
            cd['name'], cd['email'], 
            cd['password'], cd['last_4_digits'])

        if customer:
            user.stripe_id = customer.id
            user.save()
        else:
            UnpaidUsers(email=cd['email']).save()

except IntegrityError:
    form.addError(cd['email'] + ' is already a member')

Ketika UnpaidUsers menjalankan IntegrityError transaction.atomic() manajer konteks akan menangkapnya dan melakukan rollback. Pada saat kode kami dieksekusi di pengendali pengecualian, (yaitu form.addError line) rollback akan dilakukan dan kami dapat dengan aman melakukan panggilan basis data jika perlu. Perhatikan juga setiap panggilan database sebelum atau sesudah transaction.atomic() pengelola konteks tidak akan terpengaruh terlepas dari hasil akhir pengelola_konteks.



Transaksi per Permintaan HTTP

Django 1.6 (seperti 1.5) juga memungkinkan Anda untuk beroperasi dalam mode “Transaksi per permintaan”. Dalam mode ini Django akan secara otomatis membungkus fungsi tampilan Anda dalam sebuah transaksi. Jika fungsi melempar pengecualian, Django akan memutar kembali transaksi, jika tidak, ia akan melakukan transaksi.

Untuk menyiapkannya, Anda harus mengatur ATOMIC_REQUEST ke True dalam konfigurasi database untuk setiap database yang Anda inginkan untuk memiliki perilaku ini. Jadi di "settings.py" kami, kami membuat perubahan seperti ini:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'test.db'),
        'ATOMIC_REQUEST': True,
    }
}

Dalam praktiknya, ini berperilaku persis seperti jika Anda meletakkan dekorator pada fungsi tampilan kami. Jadi itu tidak sesuai dengan tujuan kami di sini.

Namun perlu diperhatikan bahwa dengan kedua ATOMIC_REQUESTS dan @transaction.atomic dekorator masih mungkin untuk menangkap/menangani kesalahan tersebut setelah dilempar dari tampilan. Untuk mengetahui kesalahan tersebut, Anda harus menerapkan beberapa middleware khusus, atau Anda dapat mengganti urls.hadler500 atau dengan membuat template 500.html.




Simpan Poin

Meskipun transaksi bersifat atomik, mereka dapat dipecah lebih lanjut menjadi savepoints. Pikirkan savepoint sebagai transaksi parsial.

Jadi, jika Anda memiliki transaksi yang memerlukan empat pernyataan SQL untuk diselesaikan, Anda dapat membuat savepoint setelah pernyataan kedua. Setelah savepoint dibuat, bahkan jika pernyataan ke-3 atau ke-4 gagal, Anda dapat melakukan rollback sebagian, menghilangkan pernyataan ke-3 dan ke-4 tetapi mempertahankan dua yang pertama.

Jadi pada dasarnya seperti membagi transaksi menjadi transaksi ringan yang lebih kecil yang memungkinkan Anda melakukan sebagian rollback atau commit.

Tetapi perlu diingat jika transaksi utama tempat untuk dibatalkan (mungkin karena IntegrityError yang dinaikkan dan tidak tertangkap, maka semua savepoint akan dikembalikan juga).

Mari kita lihat contoh cara kerja savepoint.

@transaction.atomic()
def save_points(self,save=True):

    user = User.create('jj','inception','jj','1234')
    sp1 = transaction.savepoint()

    user.name = 'starting down the rabbit hole'
    user.stripe_id = 4
    user.save()

    if save:
        transaction.savepoint_commit(sp1)
    else:
        transaction.savepoint_rollback(sp1)

Di sini seluruh fungsi ada dalam sebuah transaksi. Setelah membuat pengguna baru, kami membuat savepoint dan mendapatkan referensi ke savepoint. Tiga pernyataan berikutnya-

user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()

-bukan bagian dari savepoint yang ada, jadi mereka berpeluang menjadi bagian dari savepoint_rollback berikutnya , atau savepoint_commit . Dalam kasus savepoint_rollback , baris user = User.create('jj','inception','jj','1234') akan tetap dikomit ke database meskipun pembaruan lainnya tidak.

Dengan kata lain, dua tes berikut ini menjelaskan cara kerja savepoint:

def test_savepoint_rollbacks(self):

    self.save_points(False)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #note the values here are from the original create call
    self.assertEquals(users[0].stripe_id, '')
    self.assertEquals(users[0].name, 'jj')


def test_savepoint_commit(self):
    self.save_points(True)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #note the values here are from the update calls
    self.assertEquals(users[0].stripe_id, '4')
    self.assertEquals(users[0].name, 'starting down the rabbit hole')

Juga setelah kami melakukan atau mengembalikan savepoint kami dapat terus melakukan pekerjaan dalam transaksi yang sama. Dan pekerjaan itu tidak akan terpengaruh oleh hasil dari savepoint sebelumnya.

Misalnya jika kita memperbarui save_points our berfungsi sebagai berikut:

@transaction.atomic()
def save_points(self,save=True):

    user = User.create('jj','inception','jj','1234')
    sp1 = transaction.savepoint()

    user.name = 'starting down the rabbit hole'
    user.save()

    user.stripe_id = 4
    user.save()

    if save:
        transaction.savepoint_commit(sp1)
    else:
        transaction.savepoint_rollback(sp1)

    user.create('limbo','illbehere@forever','mind blown',
           '1111')

Terlepas dari apakah savepoint_commit atau savepoint_rollback disebut pengguna 'limbo' akan tetap berhasil dibuat. Kecuali ada hal lain yang menyebabkan seluruh transaksi dibatalkan.



Transaksi Bertingkat

Selain menentukan savepoint secara manual, dengan savepoint() , savepoint_commit , dan savepoint_rollback , membuat Transaksi bersarang akan secara otomatis membuat savepoint untuk kita, dan mengembalikannya jika kita mendapatkan kesalahan.

Memperluas contoh kita sedikit lebih jauh, kita mendapatkan:

@transaction.atomic()
def save_points(self,save=True):

    user = User.create('jj','inception','jj','1234')
    sp1 = transaction.savepoint()

    user.name = 'starting down the rabbit hole'
    user.save()

    user.stripe_id = 4
    user.save()

    if save:
        transaction.savepoint_commit(sp1)
    else:
        transaction.savepoint_rollback(sp1)

    try:
        with transaction.atomic():
            user.create('limbo','illbehere@forever','mind blown',
                   '1111')
            if not save: raise DatabaseError
    except DatabaseError:
        pass

Di sini kita dapat melihat bahwa setelah kita berurusan dengan savepoint kita, kita menggunakan transaction.atomic manajer konteks untuk membungkus kreasi pengguna 'limbo' kami. Saat pengelola konteks itu dipanggil, itu berlaku untuk membuat savepoint (karena kita sudah dalam transaksi) dan savepoint itu akan dikomit atau dibatalkan setelah keluar dari pengelola konteks.

Jadi, dua tes berikut menggambarkan perilaku mereka:

 def test_savepoint_rollbacks(self):

    self.save_points(False)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #savepoint was rolled back so we should have original values
    self.assertEquals(users[0].stripe_id, '')
    self.assertEquals(users[0].name, 'jj')

    #this save point was rolled back because of DatabaseError
    limbo = User.objects.filter(email="illbehere@forever")
    self.assertEquals(len(limbo),0)

def test_savepoint_commit(self):
    self.save_points(True)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #savepoint was committed
    self.assertEquals(users[0].stripe_id, '4')
    self.assertEquals(users[0].name, 'starting down the rabbit hole')

    #save point was committed by exiting the context_manager without an exception
    limbo = User.objects.filter(email="illbehere@forever")
    self.assertEquals(len(limbo),1)

Jadi pada kenyataannya Anda dapat menggunakan atomic atau savepoint untuk membuat savepoints di dalam transaksi. Dengan atomic Anda tidak perlu khawatir secara eksplisit tentang komit / rollback, sedangkan dengan savepoint Anda memiliki kendali penuh saat itu terjadi.



Kesimpulan

Jika Anda memiliki pengalaman sebelumnya dengan versi sebelumnya dari transaksi Django, Anda dapat melihat betapa sederhananya model transaksi tersebut. Juga memiliki AUTOCOMMIT on secara default adalah contoh yang bagus dari default "waras" yang Django dan Python keduanya banggakan dalam penyampaiannya. Untuk banyak sistem Anda tidak perlu berurusan langsung dengan transaksi, biarkan AUTOCOMMIT melakukan pekerjaannya. Tetapi jika Anda melakukannya, semoga posting ini memberi Anda informasi yang Anda butuhkan untuk mengelola transaksi di Django seperti seorang profesional.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Pengantar HDFS | Apa itu HDFS dan Bagaimana Cara Kerjanya?

  2. ScaleGrid Terpilih untuk Program Penghargaan Cloud 2017-2018

  3. Tren Perangkat Keras Server Basis Data

  4. Tips Wawancara Administrator Database SQL

  5. Relasional vs basis data non-relasional - Part 3