MariaDB ve MySQL’de JSON_SET ile JSON Veri Güncelleme

Modern web uygulamalarının büyük çoğunluğu artık veritabanında JSON formatında veri saklıyor. Kullanıcı tercihleri, uygulama ayarları, dinamik form verileri… Bunların hepsini ayrı sütunlarda tutmak yerine tek bir JSON sütununda saklamak hem esneklik sağlıyor hem de şema değişikliklerinin önüne geçiyor. Peki ya bu verileri güncellemek gerektiğinde ne yapıyoruz? İşte tam bu noktada JSON_SET fonksiyonu devreye giriyor.

Bu yazıda MariaDB ve MySQL’de JSON_SET fonksiyonunu gerçek dünya senaryolarıyla ele alacağız. Temel söz diziminden başlayıp karmaşık iç içe yapılara, performans ipuçlarına kadar her şeyi konuşacağız.

JSON_SET Nedir ve Neden Kullanıyoruz?

JSON_SET, mevcut bir JSON değeri içinde belirli bir yolu hedef alarak değer atamanı sağlayan bir SQL fonksiyonudur. Temel mantığı şu: JSON belgenin tamamını çekip uygulama katmanında değiştirip geri yazmak yerine, doğrudan veritabanı seviyesinde istediğin alanı güncelliyorsun.

Bu yaklaşımın birkaç somut avantajı var:

  • Ağ trafiği azalır: Büyük JSON belgeleri için tüm veriyi çekip geri göndermek yerine sadece değişen kısmı işliyorsun
  • Race condition riski düşer: Uygulama katmanında oku-değiştir-yaz döngüsü yerine atomik bir veritabanı işlemi yapıyorsun
  • Kod daha temiz kalır: Uygulama kodunda JSON manipülasyonu yapmak zorunda kalmıyorsun

MySQL 5.7.8 ve MariaDB 10.2.3 sürümlerinden itibaren native JSON desteği geldi. Bu sürümlerin altındaysanız önce versiyon yükseltmesini düşünün, çünkü JSON fonksiyonları olmadan bu işlemleri yapmak ciddi acı veriyor.

Temel Söz Dizimi

JSON_SET fonksiyonunun genel yapısı şöyle:

JSON_SET(json_belgesi, yol, deger [, yol, deger] ...)

Burada:

  • json_belgesi: Üzerinde işlem yapacağın JSON değeri veya sütun adı
  • yol: Güncellemek istediğin alanın JSON path ifadesi (dollar işaretiyle başlar)
  • deger: Atamak istediğin yeni değer

Birden fazla alan-değer çifti girebilirsin, fonksiyon bunları soldan sağa sırayla işler.

JSON_SET ile kardeş fonksiyonları arasındaki farkı da bilmek lazım:

  • JSON_SET: Yol mevcutsa günceller, yoksa ekler
  • JSON_INSERT: Sadece yol yoksa ekler, varsa dokunmaz
  • JSON_REPLACE: Sadece yol varsa günceller, yoksa dokunmaz

Bu farkı bilmeden yazdığın sorgular beklenmedik sonuçlar üretebilir.

Basit Güncelleme Örnekleri

Önce bir test ortamı kuralım. Kullanıcı tercihlerini JSON olarak sakladığımız klasik bir senaryo:

CREATE TABLE kullanicilar (
    id INT PRIMARY KEY AUTO_INCREMENT,
    kullanici_adi VARCHAR(100) NOT NULL,
    tercihler JSON,
    olusturma_tarihi TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO kullanicilar (kullanici_adi, tercihler) VALUES
('ahmet_k', '{"tema": "acik", "dil": "tr", "bildirimler": true, "sayfa_boyutu": 20}'),
('fatma_s', '{"tema": "koyu", "dil": "en", "bildirimler": false, "sayfa_boyutu": 50}'),
('mehmet_y', '{"tema": "acik", "dil": "tr", "bildirimler": true, "sayfa_boyutu": 10}');

Şimdi basit bir güncelleme yapalım. Ahmet’in temasını karanlık moda alalım:

UPDATE kullanicilar
SET tercihler = JSON_SET(tercihler, '$.tema', 'koyu')
WHERE kullanici_adi = 'ahmet_k';

-- Sonucu kontrol edelim
SELECT kullanici_adi, JSON_EXTRACT(tercihler, '$.tema') AS tema
FROM kullanicilar
WHERE kullanici_adi = 'ahmet_k';

Aynı anda birden fazla alanı güncellemek de mümkün:

UPDATE kullanicilar
SET tercihler = JSON_SET(
    tercihler,
    '$.tema', 'koyu',
    '$.sayfa_boyutu', 100,
    '$.bildirimler', false
)
WHERE kullanici_adi = 'mehmet_y';

Bu şekilde tek sorguyla üç farklı JSON alanını güncelledik. Uygulama katmanında bunu yapmak için önce veriyi çekip sonra birleştirip sonra geri yazmak gerekirdi.

İç İçe JSON Yapılarını Güncelleme

Gerçek dünyada JSON yapıları genellikle çok daha karmaşık oluyor. Nested (iç içe) objeler ve diziler sıkça karşılaşılan durumlar:

CREATE TABLE urun_katalogu (
    id INT PRIMARY KEY AUTO_INCREMENT,
    urun_kodu VARCHAR(50) NOT NULL,
    ozellikler JSON
);

INSERT INTO urun_katalogu (urun_kodu, ozellikler) VALUES
('LAPTOP-001', '{
    "marka": "TechBrand",
    "teknik": {
        "ram": 16,
        "depolama": 512,
        "ekran": {
            "boyut": 15.6,
            "cozunurluk": "1920x1080"
        }
    },
    "fiyat": {
        "liste": 25000,
        "indirimli": null
    },
    "etiketler": ["dizustu", "is", "performans"]
}');

İç içe bir alanı güncellemek için nokta notasyonunu kullanıyoruz:

-- RAM değerini güncelle
UPDATE urun_katalogu
SET ozellikler = JSON_SET(ozellikler, '$.teknik.ram', 32)
WHERE urun_kodu = 'LAPTOP-001';

-- Ekran çözünürlüğünü güncelle (üç seviye derinlik)
UPDATE urun_katalogu
SET ozellikler = JSON_SET(ozellikler, '$.teknik.ekran.cozunurluk', '2560x1440')
WHERE urun_kodu = 'LAPTOP-001';

-- Hem derinlerdeki bir alanı hem de üst seviyeyi aynı anda güncelle
UPDATE urun_katalogu
SET ozellikler = JSON_SET(
    ozellikler,
    '$.fiyat.indirimli', 22000,
    '$.teknik.depolama', 1024
)
WHERE urun_kodu = 'LAPTOP-001';

JSON Dizileriyle Çalışmak

Diziler biraz daha dikkat gerektiriyor. Index tabanlı erişim kullanıyoruz ve indeksler sıfırdan başlıyor:

CREATE TABLE siparisler (
    id INT PRIMARY KEY AUTO_INCREMENT,
    musteri_id INT NOT NULL,
    siparis_detay JSON
);

INSERT INTO siparisler (musteri_id, siparis_detay) VALUES
(1, '{
    "durum": "beklemede",
    "urunler": [
        {"kod": "A001", "adet": 2, "fiyat": 150},
        {"kod": "B002", "adet": 1, "fiyat": 300},
        {"kod": "C003", "adet": 3, "fiyat": 75}
    ],
    "kargo": {
        "firma": "HizliKargo",
        "takip_no": null
    }
}');

Dizi elemanlarına erişim ve güncelleme:

-- İlk ürünün (index 0) adedini güncelle
UPDATE siparisler
SET siparis_detay = JSON_SET(siparis_detay, '$.urunler[0].adet', 5)
WHERE id = 1;

-- Kargo takip numarasını ekle ve durumu güncelle
UPDATE siparisler
SET siparis_detay = JSON_SET(
    siparis_detay,
    '$.kargo.takip_no', 'HK-2024-789456',
    '$.durum', 'kargoda'
)
WHERE id = 1;

-- Güncellemeyi doğrula
SELECT
    JSON_EXTRACT(siparis_detay, '$.durum') AS durum,
    JSON_EXTRACT(siparis_detay, '$.kargo.takip_no') AS takip_no,
    JSON_EXTRACT(siparis_detay, '$.urunler[0].adet') AS ilk_urun_adet
FROM siparisler
WHERE id = 1;

Koşullu Güncelleme Senaryoları

Bazen sadece belirli koşullar sağlandığında JSON alanını güncellemek istiyoruz. JSON fonksiyonlarını WHERE koşullarıyla birleştirmek güçlü sonuçlar üretiyor:

-- Sadece karanlık tema kullananların sayfa boyutunu 100 yap
UPDATE kullanicilar
SET tercihler = JSON_SET(tercihler, '$.sayfa_boyutu', 100)
WHERE JSON_EXTRACT(tercihler, '$.tema') = 'koyu';

-- Bildirimleri açık olan kullanıcılara yeni bir tercih ekle
UPDATE kullanicilar
SET tercihler = JSON_SET(tercihler, '$.ses_bildirimi', true)
WHERE JSON_EXTRACT(tercihler, '$.bildirimler') = true;

-- JSON değeri null olan kayıtları güncelle
UPDATE urun_katalogu
SET ozellikler = JSON_SET(ozellikler, '$.fiyat.indirimli', 
    JSON_EXTRACT(ozellikler, '$.fiyat.liste') * 0.9)
WHERE JSON_EXTRACT(ozellikler, '$.fiyat.indirimli') IS NULL;

Son örnekte ilginç bir şey var: JSON_SET ile dinamik hesaplama yapıyoruz. Liste fiyatının yüzde doksanını indirimli fiyat olarak otomatik atadık.

Gerçek Dünya Senaryosu: Uygulama Konfigürasyon Yönetimi

Birçok SaaS uygulamasında her müşteri için farklı konfigürasyonlar tutulur. Bu senaryoyu ele alalım:

CREATE TABLE musteri_konfig (
    id INT PRIMARY KEY AUTO_INCREMENT,
    musteri_kodu VARCHAR(50) UNIQUE NOT NULL,
    plan VARCHAR(20) DEFAULT 'baslangic',
    ayarlar JSON,
    son_guncelleme TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO musteri_konfig (musteri_kodu, plan, ayarlar) VALUES
('MUS-001', 'profesyonel', '{
    "limitler": {
        "kullanici_sayisi": 50,
        "depolama_gb": 100,
        "api_cagrisi_gunluk": 10000
    },
    "ozellikler": {
        "raporlama": true,
        "api_erisimi": true,
        "ozel_domain": false,
        "sso": false
    },
    "bildirim_ayarlari": {
        "email": true,
        "sms": false,
        "webhook_url": null
    }
}'),
('MUS-002', 'baslangic', '{
    "limitler": {
        "kullanici_sayisi": 5,
        "depolama_gb": 10,
        "api_cagrisi_gunluk": 500
    },
    "ozellikler": {
        "raporlama": false,
        "api_erisimi": false,
        "ozel_domain": false,
        "sso": false
    },
    "bildirim_ayarlari": {
        "email": true,
        "sms": false,
        "webhook_url": null
    }
}');

Müşteri plan yükseltme işlemi:

-- MUS-002 müşterisi profesyonel plana geçiyor
UPDATE musteri_konfig
SET
    plan = 'profesyonel',
    ayarlar = JSON_SET(
        ayarlar,
        '$.limitler.kullanici_sayisi', 50,
        '$.limitler.depolama_gb', 100,
        '$.limitler.api_cagrisi_gunluk', 10000,
        '$.ozellikler.raporlama', true,
        '$.ozellikler.api_erisimi', true
    )
WHERE musteri_kodu = 'MUS-002';

-- Webhook URL'i ekle (yeni alan)
UPDATE musteri_konfig
SET ayarlar = JSON_SET(
    ayarlar,
    '$.bildirim_ayarlari.webhook_url', 'https://musteri002.com/webhook',
    '$.bildirim_ayarlari.sms', true
)
WHERE musteri_kodu = 'MUS-002';

-- Tüm müşterilere yeni bir özellik ekle (migration senaryosu)
UPDATE musteri_konfig
SET ayarlar = JSON_SET(
    ayarlar,
    '$.ozellikler.dark_mode', false,
    '$.ozellikler.beta_ozellikleri', false
);

Son satır özellikle dikkat çekici: WHERE koşulu olmadan tüm kayıtlara aynı anda yeni JSON alanları ekledik. Bu tam anlamıyla bir şema migration işlemi ve bunu hiçbir şema değişikliği yapmadan gerçekleştirdik.

JSON_SET ile SELECT Kullanımı

JSON_SET sadece UPDATE sorgularında değil, SELECT içinde de kullanılabilir. Geçici dönüşümler yapmak için işe yarıyor:

-- Veriyi güncellemeden nasıl görüneceğini test et
SELECT
    musteri_kodu,
    JSON_EXTRACT(ayarlar, '$.limitler.kullanici_sayisi') AS mevcut_limit,
    JSON_EXTRACT(
        JSON_SET(ayarlar, '$.limitler.kullanici_sayisi', 200),
        '$.limitler.kullanici_sayisi'
    ) AS yeni_limit
FROM musteri_konfig
WHERE musteri_kodu = 'MUS-001';

-- Raporlama için geçici hesaplamalar
SELECT
    musteri_kodu,
    JSON_UNQUOTE(JSON_EXTRACT(ayarlar, '$.limitler.depolama_gb')) AS mevcut_depolama,
    JSON_UNQUOTE(
        JSON_EXTRACT(
            JSON_SET(ayarlar, '$.limitler.depolama_gb',
                JSON_EXTRACT(ayarlar, '$.limitler.depolama_gb') * 2
            ),
            '$.limitler.depolama_gb'
        )
    ) AS iki_katli_depolama
FROM musteri_konfig;

Performans Dikkat Noktaları

JSON kolonları üzerinde yoğun işlem yapıyorsan birkaç noktayı aklında tutman lazım.

Index kullanımı: JSON alanlarında directly index oluşturamazsın ama generated column ile bunu aşabilirsin:

-- Sık sorgulanan JSON alanı için generated column ve index
ALTER TABLE kullanicilar
ADD COLUMN tema VARCHAR(20) GENERATED ALWAYS AS 
    (JSON_UNQUOTE(JSON_EXTRACT(tercihler, '$.tema'))) VIRTUAL,
ADD INDEX idx_tema (tema);

-- Artık bu sorgu index kullanır
SELECT * FROM kullanicilar WHERE tema = 'koyu';

Büyük JSON belgelerinde dikkat: JSON_SET her seferinde tüm JSON belgesini yeniden serialize ediyor. Çok büyük JSON belgelerinde (birkaç MB) bu işlem maliyetli olabilir. Böyle durumlarda JSON’u parçalamayı veya ayrı tablolara taşımayı düşün.

Transaction içinde kullan: Birden fazla JSON güncellemesini aynı anda yapıyorsan bunları bir transaction içine al:

START TRANSACTION;

UPDATE musteri_konfig
SET ayarlar = JSON_SET(ayarlar, '$.ozellikler.sso', true)
WHERE musteri_kodu = 'MUS-001';

UPDATE kullanicilar
SET tercihler = JSON_SET(tercihler, '$.sso_aktif', true)
WHERE id IN (SELECT id FROM kullanici_musteri_map WHERE musteri_kodu = 'MUS-001');

COMMIT;

Hata Ayıklama ve Doğrulama

Canlı sisteme uygulamadan önce sorgularını test et. Birkaç pratik ipucu:

-- JSON yapısını doğrula
SELECT JSON_VALID('{"anahtar": "deger"}');  -- 1 döner
SELECT JSON_VALID('{"bozuk: json}');  -- 0 döner

-- Güncelleme öncesi mevcut değeri gör
SELECT JSON_EXTRACT(tercihler, '$.tema') FROM kullanicilar WHERE id = 1;

-- Güncellemeyi önce SELECT ile simüle et
SELECT JSON_SET(tercihler, '$.tema', 'yeni_deger') 
FROM kullanicilar 
WHERE id = 1;

-- JSON path'i doğrula
SELECT JSON_EXTRACT('{"a": {"b": {"c": 42}}}', '$.a.b.c');  -- 42 döner

-- NULL kontrolü
SELECT 
    kullanici_adi,
    CASE 
        WHEN JSON_EXTRACT(tercihler, '$.tema') IS NULL THEN 'Tema yok'
        ELSE JSON_UNQUOTE(JSON_EXTRACT(tercihler, '$.tema'))
    END AS tema_durumu
FROM kullanicilar;

Yaygın Hatalar ve Çözümleri

  • Yanlış path söz dizimi: Dollar işareti ($) unutulursa veya nokta yerine slash kullanılırsa sorgu hata verir ya da NULL döner. Her zaman $.alan_adi formatını kullan.
  • Tip uyumsuzluğu: JSON’da sayısal değer tutan bir alana string atarsan karşılaştırmalar bozulur. JSON_EXTRACT sayısal değerler için tırnak olmadan, string değerler için tırnaklı döner. JSON_UNQUOTE kullanarak bunu normalize edebilirsin.
  • NULL JSON belgesi: Eğer JSON sütunu NULL ise JSON_SET yine NULL döner. Önce COALESCE ile boş obje atayabilirsin:
UPDATE kullanicilar
SET tercihler = JSON_SET(
    COALESCE(tercihler, '{}'),
    '$.yeni_alan', 'deger'
)
WHERE tercihler IS NULL;
  • Encoding sorunları: Türkçe karakterler JSON içinde düzgün saklanır ama tablo karakter seti UTF8MB4 olmalı. Özellikle emoji ve özel karakterler için bu kritik.

Sonuç

JSON_SET, veritabanında JSON veri saklayan uygulamalar için gerçekten güçlü bir araç. Uygulama katmanında JSON manipülasyonu yapmak yerine veritabanı seviyesinde atomik güncellemeler yapmanı sağlıyor. Özellikle konfigürasyon yönetimi, kullanıcı tercihleri ve dinamik form verileri gibi senaryolarda işi büyük ölçüde kolaylaştırıyor.

En önemli noktaları özetleyelim:

  • JSON_SET mevcut değeri günceller, yoksa ekler. JSON_INSERT ve JSON_REPLACE ile farkını bilmek kritik.
  • Tek sorguyla birden fazla JSON alanını güncelleyebilirsin, virgülle ayırman yeterli.
  • İç içe yapılar için nokta notasyonu, diziler için köşeli parantez ve index kullanıyorsun.
  • Sık sorgulanan JSON alanları için generated column ve index kombinasyonunu kullan.
  • Büyük ölçekli güncellemeleri transaction içinde yap.
  • Canlıya almadan önce SELECT ile simülasyon yap.

JSON kolonları kullanmak şema esnekliği sağlıyor ama performans tuzaklarına dikkat etmek gerekiyor. Her şeyi JSON’a doldurmak yerine gerçekten dinamik olan verileri JSON’da, sabit yapıdaki verileri normal sütunlarda tutmak en sağlıklı yaklaşım. Bu dengeyi kurduğunda hem esneklikten hem de veritabanı performansından tam olarak yararlanıyorsun.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir