Rest API Mimarisinin API ve Veritabanı Ayağı Hakkında

Selamlar,
Ben Rest API mimarisini öğrenmek için Python’ın Flask framework’ünü ve Sqlite veritabanını kullanarak basit bir uygulama yapıyorum. Uygulamanın olayı temel olarak, girişini yaptığımız harcama kalemleri doğrultusunda ne kadar bakiyemizin kaldığını göstermek, kişisel bir gider defteri gibi.

Şu ana kadar hazırladığım kısmı, veritabanı işlemlerini (eğer yoksa tablo oluşturmak ve CRUD) gerçekleştirecek kodlar ve veritabanı ile iletişim kurmayı sağlayacak API’ın kodları.


Veritabanındaki tablonun yapısı:
Screenshot from 2023-08-10 17-24-38


Veritabanı işlemlerinin gerçekleştiği kısım:

db.py
#!/usr/bin/env python3

import sqlite3
import logging

logging.basicConfig(level=logging.DEBUG)

class DBUtil:
    ''' Database class for OOP architecture. '''
    def __init__(self, db_name, table_name):
        self.db_name = db_name
        self.table_name = table_name

        try:
            self.connection = sqlite3.connect(db_name, check_same_thread=False)
            self.cursor = self.connection.cursor()
            self.create_table_if_not_exists()
            
        except sqlite3.Error as err:
            logging.error(err)


    def create_table_if_not_exists(self):
        ''' Create table. '''

        query = f"CREATE TABLE IF NOT EXISTS {self.table_name} (id INTEGER PRIMARY KEY, \
        item_name VARCHAR(256) NOT NULL, category VARCHAR(256) NOT NULL, price REAL, \
        payment_method VARCHAR(256) NOT NULL)"
        try:
            self.cursor.execute(query)

        except sqlite3.Error as err:
            logging.error(err)


    def insert_data(self, item_name: str, category: str, price: float, payment_method: str):
        ''' Insert data.
        item_name: Bought item name
        category: Bought item category
        price: Bought item price
        payment_method: Bought item payment method '''
        

        query = f"INSERT INTO {self.table_name} \
        (item_name, category, price, payment_method) VALUES (?,?,?,?)"

        try:
            self.cursor.execute(query, (item_name, category, price, payment_method))          
            self.connection.commit()
  
        except sqlite3.Error as err:
            logging.error(err)

        self.cursor.execute(f"SELECT * FROM {self.table_name} WHERE id == last_insert_rowid();")
        result = self.cursor.fetchone()
        logging.info(f"Row inserted: {result}")

        return result


    def get_all_data(self):
        ''' Get all data.'''

        query = f"SELECT * FROM {self.table_name};"
        results = None
        try:
            self.cursor.execute(query)
            self.connection.commit()
            results = self.cursor.fetchall()

        except sqlite3.Error as err:
            logging.error(err)

        return results
            


    def get_data(self, item_id):
        ''' Get specific data.'''

        query = f"SELECT * FROM {self.table_name} WHERE id == ?"
        try:
            self.cursor.execute(query, (item_id,))
            result = self.cursor.fetchone()

        except sqlite3.Error as err:
            logging.error(err)

        return result


    def update_data(self, item_id, update_column, new_data):
        ''' Update data. '''

        query = f"UPDATE {self.table_name} SET {update_column} = ? WHERE id == ?"
        try:
            self.cursor.execute(query, (new_data, item_id))
            self.connection.commit()

        except sqlite3.Error as err:
            logging.error(err)

        updated_row = self.get_data(item_id)
        return updated_row


    def delete_data(self, item_id):
        ''' Delete data. '''
        deleted_row = self.get_data(item_id)
        query = f"DELETE FROM {self.table_name} WHERE id == ?"
        try:
            self.cursor.execute(query, (item_id,))
            self.connection.commit()

        except sqlite3.Error as err:
            logging.error(err)

        return deleted_row



API kodları:

api.py
from flask import Flask, jsonify, request
from db import DBUtil

db_agent = DBUtil("pocketDB", "pocket_table")

app = Flask(__name__)

@app.route("/pocket/getAllData", methods=["GET"])
def get_all_data():
        # TODO: data'yi json olarak dondur
        all_data = db_agent.get_all_data()
        json_data = []
        for item in all_data:
            json_data.append({"id": item[0],
                            "item_name": item[1],
                            "category": item[2],
                            "price": item[3],
                            "payment_method": item[4]})
        
        return jsonify(json_data)
    

@app.route("/pocket/getData/<int:item_id>", methods=["GET"])
def get_data(item_id):
        
        # TODO: data'yi json olarak dondur
        item_data = db_agent.get_data(item_id)
        json_data = {"id": item_data[0],
                            "item_name": item_data[1],
                            "category": item_data[2],
                            "price": item_data[3],
                            "payment_method": item_data[4]}
        
        return jsonify(json_data)


@app.route("/pocket", methods=["POST"])
def post_data():
    item_name = request.json["item_name"]
    category = request.json["category"]
    price = request.json["price"]
    payment_method = request.json["payment_method"]


    response = db_agent.insert_data(item_name, category, price, payment_method)
    json_data = {
        "id": response[0],
        "item_name": response[1],
        "category": response[2],
        "price": response[3],
        "payment_method": response[4]
    }

    return jsonify(json_data)


@app.route("/pocket", methods=["PUT"])
def put_data():
    item_id = request.json["id"]
    update_column = request.json["update_column"]
    new_data = request.json["new_data"]

    response = db_agent.update_data(item_id, update_column, new_data)
    json_data = {
        "id": response[0],
        "item_name": response[1],
        "category": response[2],
        "price": response[3],
        "payment_method": response[4]
    }

    return jsonify(json_data)


@app.route("/pocket/<int:item_id>", methods=["DELETE"])
def delete_data(item_id):
    response = db_agent.delete_data(item_id)
    json_data = {
        "id": response[0],
        "item_name": response[1],
        "category": response[2],
        "price": response[3],
        "payment_method": response[4]
    }

    return jsonify(json_data)

if __name__ == "__main__":
    app.run(debug=False)
        


Sorularım:

  1. Veritabanı işlemleri için db.py’daki DBUtil sınıfından nesneyi api.py’da üretmem doğru mu? Yanlış gelmesinin sebebi, api.py’da yalnızca logic işlemlerini gerçekleştirmem, herhangi bir implementasyon yapmamam gerektiğini düşünmem. Ama bu durumda da örneğin tüm datayı çekmek için gerekli all_data = db_agent.get_all_data() gibi bir satırda, db_agent’i nasıl çağıracağım?

  2. Veritabanından gelen SQL sorgusu cevabını, db.py’da, veritabanından geldiği gibi mi; yoksa api.py’da API Server’a sunarken mı JSON’a çevirmem lazım? Yani

        item_data = db_agent.get_data(item_id)
        # >>> (1, 'gofret', 'gida', 21.45, 'nakit')

        json_data = {"id": item_data[0],
                            "item_name": item_data[1],
                            "category": item_data[2],
                            "price": item_data[3],
                            "payment_method": item_data[4]}
        # >>> {'id': 1, 'item_name': 'gofret', 'category': 'gida', 'price': 21.45, 'payment_method': 'nakit'}
        
        return jsonify(json_data)

şu dönüştürme api.py’da mı yapılmalı db.py’da mı?

  1. Her bir CRUD işlemini (commit, execute, connection vs. gerektiren) try-catch’e sarmam doğru mu? Ancak bu şekilde düzgün loglama yapabileceğimi düşündüm.

  2. Class yapısını kullanış şeklimde bir terslik var mı? “Bu böyle kullanılmaz” veya çok daha basit halledilebilecek şeyi gereksizce uzatmış olabileceğim bir durum gibi.

  3. Şu anda API ve DB iletişim kurup CRUD operasyonları yapabiliyor ancak ben çok yetersiz olduğunu düşünüp, eksiklerimin ne olduğunu göremiyorum. Çalışıyor ama bu doğru yol değil sadece bir workaround yapmışsın diyeceğiniz veya gözünüze çarpan herhangi bir usülsüzlüğü bildirirseniz çok sevinirim.

Vakit ayırdığınız için şimdiden teşekkür ederim.

REST’in orijinal tanimina mi sadik kaliyorsunuz, yoksa HTTP filleri kullanan ve obje donduren bir uygulama mi yaziyorsunuz? Orijinal tanimina cok hakim degilim; tek soyleyebilecegim obje grafigini alip API endpoint’leri uretecek –ORM gibi– otomatik tooling kullanmanin faydali ve belki gerekli olacagi.

JSON over HTTP bir API yaziyorsaniz bu herhangi bir mimaride olabilir; yazilana yakin mimarilerden ve genel gecer best practice’lerden bahsedecegim.

Normalde ikisini de ucuncu bir modulde uretip orada birbirine baglamak dogrusu (dependency inversion principle) fakat api’nin DBUtil’i uretmesinden daha buyuk bir sorun DBUtil’de tanimlanan objenin detaylarina bagli olmasi.

Yani zaten DBUtil’i cikartip yerine baska bir sinif koymak mumkun olmayacagi icin aslinda bu iki modul (veya modul+sinif) birbirinden bagimsiz degil. O yuzden birinin digerini uretmesinde veya ayni dosyada bulunmalarinda bile bir sakinca yok.

Bunlari daha da ayirmak icin Pocket bir business objesine donusturulebilir, al, ver, vs. fonksiyonlari Pocket alacak sekilde degistirilebilir.

Ikisi de lazim degil.

Fakat JSON ciktisi istiyorsak HTTP handler’dan JSON cikmasi lazim.

db↔api arasinda ne gectigi kimseyi ilgilendirmiyor. Cok katmanli mimari istiyorsak yukarida dedigim gibi bir business objesi gecebilir.

Evet; python’da hata kontrolu boyle yapiliyor.

DBUtil’e daha iyi bir isim lazim. Manage ettigi objelerin kolonlarina kadar bildigine gore genel bir “DB utility” class’i degil, birinin data access/persistance layer’i.

Constructor’da is yapmak genel olarak iyi bir fikir degil; objeyi idare etmeyi zor hale getiriyor. (Okey oldugunu dusunenler de var; arastirilabilir.)

def insert_data(self, item_name: str, category: str, price: float, payment_method: str):

Cok parametre alan methodlara tekrar bakmak gerekiyor. Parametreleri gruplamak mantikli olabilir. (Ki burada yukarida bahsettigim Pocket objesine denk geliyor.)

api.py’nin main fonksiyonu module dagilmis durumda. Flask’in dekoratorleri baska sekilde kullanilmiyor ama. Cozumu ne bilmiyorum. (Her seyi main icine almak?)

Varsa 1-2 obje/tablo daha ekleyince ortaya cikacaktir. Cikmazsa her sey yolundadir zaten?

1 Beğeni

Tam da ihtiyacım olan değerlendirmeler @aib hocam teşekkür ederim :pray:t3:

Merhabalar,

Bu arada, Flask, PostgreSQL, SQLAlchemy, Flask-SQLAlchemy kütüphaneleri kullanılarak oluşturulmuş, http request metodlarına uygun JSON cevaplar gönderen, ORM tabanlı basit bir RESTful API uygulaması örneği paylaşmak isterim.

SQLAlchemy ve Flask-SQLAlchemy’yi sqlite3 ile de kullanabilirsiniz ancak PostgreSQL’i kullanmaya başlayıp, sağladığı imkanları gördükten sonra tercih etmeye başlayacağınızı düşünüyorum. Ayrıca tablolara karmaşık JSON verileri de ekleyebilirsiniz.

Kodları paylaşmadan önce gerekli kütüphaneleri listeleyeyim:

python -m pip install flask psycopg2 Flask-SQLAlchemy

psycopg2’yi kullanabilmek için PostgreSQL’in yüklü olması gerektiğini hatırlatmak isterim. PostgreSQL’i kurduktan sonra bir kullanıcı adı, şifre, bir veritabanı ve bir tablo oluşturmanız gerekiyor.

Genellikle kurulumdan sonra aşağıdaki ifadeyle PostgreSQL konsoluna giriş yaparsınız:

psql -U postgres

Yukardaki ifadeden sonra cli uygulamasına giriş yaparsınız. Sonra da bir kullanıcı adı ve şifre oluşturun kendinize:

CREATE USER serhank WITH PASSWORD '123456' CREATEDB;

Sonra oluşturduğunuz kullanıcı adıyla oturum açın:

SET SESSION AUTHORIZATION 'serhank';

Sonra bir tane veritabanı oluşturun:

CREATE DATABASE test;

Sonra da bir tane tablo oluşturun:

CREATE TABLE IF NOT EXISTS test (id SERIAL PRIMARY KEY, data JSON);

Şimdi, PostgreSQL’in CLI uygulamasından çıkabiliriz.

Bu arada, ben bu kullanıcı adı, şifre, veritabanı ismi, host, port gibi bilgileri ortam değişkeni olarak saklıyorum.

Değerler de şöyle:

host = os.environ["POSTGRES_HOST"]
port = os.environ["POSTGRES_PORT"]
username = os.environ["POSTGRES_USERNAME"]
password = os.environ["POSTGRES_PASSWORD"]
database = os.environ["POSTGRES_DATABASE"]

Kodlarda yer alan bu ifadeleri kendinize göre değiştirirsiniz.

Şimdi izninizle dosyaları paylaşayım:

app.py
import os
import json

from sqlalchemy import and_
from models import db, Model
from flask import Flask, request, jsonify

host = os.environ["POSTGRES_HOST"]
port = os.environ["POSTGRES_PORT"]
username = os.environ["POSTGRES_USERNAME"]
password = os.environ["POSTGRES_PASSWORD"]
database = "test"

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = f"postgresql://{username}:{password}@{host}:{port}/{database}"

with app.app_context():
    db.init_app(app)
    db.drop_all()
    db.create_all()


@app.route("/", methods=["GET"])
def get():
    args = json.loads(request.data.decode("utf-8"))
    if all(arg in ["category", "name"] and isinstance(args[arg], str) for arg in args):
        query = Model.query.filter(and_(*map(lambda arg: Model.data[arg].astext == args[arg], args))).all()
        if query:
            return jsonify([i.data for i in query])
        else:
            return {"status": 404, "message": f"Not Found: {args['category']} not found"}
    return {"status": 400, "message": "Bad Request"}


@app.route("/", methods=["POST"])
def post():
    args = json.loads(request.data.decode("utf-8"))
    if all(arg in ["name", "category"] and isinstance(args[arg], str) for arg in args):
        instance = Model.query.filter(Model.data["name"].astext == args["name"]).first()
        if not instance:
            try:
                db.session.add(Model(data=args))
                db.session.commit()
                return {"status": 201}
            except Exception as e:
                db.session.rollback()
                return {"status": 500, "message": f"Internal Server Error: {e}"}
        else:
            return {"status": 409, "message": f"Conflict: {args['name']} already exists"}
    return {"status": 400, "message": "Bad Request"}


@app.route("/", methods=["PUT"])
def put():
    args = json.loads(request.data.decode("utf-8"))
    if all(arg in ["name", "data"] for arg in args):
        instance = Model.query.filter(Model.data["name"].astext == args["name"]).first()
        if instance:
            try:
                db.session.query(Model).filter(Model.data["name"].astext == instance.data["name"]).update({"data": args["data"]})
                db.session.commit()
                return {"status": 200}
            except Exception as e:
                db.session.rollback()
                return {"status": 500, "message": f"Internal Server Error: {e}"}
        else:
            try:
                db.session.add(Model(data=args["data"]))
                db.session.commit()
                return {"status": 201}
            except Exception as e:
                db.session.rollback()
                return {"status": 500, "message": f"Internal Server Error: {e}"}
    return {"status": 400, "message": "Bad Request"}


@app.route("/", methods=["PATCH"])
def patch():
    args = json.loads(request.data.decode("utf-8"))
    if all(arg in ["name", "data"] for arg in args):
        instance = Model.query.filter(Model.data["name"].astext == args["name"]).first()
        if instance:
            try:
                db.session.query(Model).filter(Model.data["name"].astext == instance.data["name"]).update({"data": args["data"]})
                db.session.commit()
                return {"status": 200}
            except Exception as e:
                db.session.rollback()
                return {"status": 500, "message": f"Internal Server Error: {e}"}
        else:
            return {"status": 404, "message": f"Not Found: {args['name']} not found"}
    return {"status": 400, "message": "Bad Request"}


@app.route("/", methods=["DELETE"])
def delete():
    args = json.loads(request.data.decode("utf-8"))
    if list(args) == ["name"] and isinstance(args["name"], str):
        instance = Model.query.filter(Model.data["name"].astext == args["name"]).first()
        if instance:
            try:
                db.session.delete(instance)
                db.session.commit()
                return {"status": 204}
            except Exception as e:
                db.session.rollback()
                return {"status": 500, "message": f"Internal Server Error: {e}"}
        else:
            return {"status": 404, "message": f"Not Found: {args['name']} not found"}
    return {"status": 400, "message": "Bad Request"}


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)


model.py
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.postgresql import JSON

db = SQLAlchemy()


class Model(db.Model):
    __tablename__ = "test"
    id = db.Column(db.Integer, primary_key=True)
    data = db.Column(JSON, nullable=False)

    def __init__(self, data):
        self.data = data

Server’ı çalıştırıp, başka bir dosyadan localhost’a http istek metodları gönderelim.

import json
import requests

post = requests.post("http://127.0.0.1:5000", data=json.dumps({"category": "sebze", "name": "patlıcan"}))
print(post.json())

post = requests.post("http://127.0.0.1:5000", data=json.dumps({"category": "sebze", "name": "pırasa"}))
print(post.json())

get = requests.get("http://127.0.0.1:5000", data=json.dumps({"category": "sebze"}))
print(get.json())

put = requests.put("http://127.0.0.1:5000", data=json.dumps({"name": "enginar", "data": {"category": "meyve", "name": "enginar"}}))
print(put.json())

patch = requests.patch("http://127.0.0.1:5000", data=json.dumps({"name": "enginar", "data": {"category": "sebze", "name": "enginar"}}))
print(patch.json())

delete = requests.delete("http://127.0.0.1:5000", data=json.dumps({"name": "enginar"}))
print(delete.json())

Çıktı:

{'status': 201}
{'status': 201}
[{'category': 'sebze', 'name': 'patlıcan'}, {'category': 'sebze', 'name': 'pırasa'}]
{'status': 201}
{'status': 200}
{'status': 204}

Umarım faydası olur. Herkese iyi çalışmalar.

2 Beğeni

PostgreSQL, SQLAlchemy ve ORM yapısı ile çalışmak henüz tecrübe etmediğim şeyler. Ama sizin yazdığınız veritabanı kodu benimkinden çok daha temiz görünüyor. Yani bu şekilde yapmaya benim de alışmam gerekiyor, anladığım.

Bu konseptleri anlayıp kendi koduma entegre etmeye çalışacağım.

Ben mesela fark ettiğiniz üzere dümdüz sqlite bağlantısı oluşturup onu öylece çağırdım, arada güvenliğe dair (yapılması gereken ne var onu da bilmiyorum gerçi ama) bir tedbir bulundurmadım.

Bu gibi problemleri standartlaşmış yöntemleri kullanarak çözeceğime de inanıyorum. Daha önemlisi, neyin eksik olduğuna bu gibi yöntemlerin varlığından haberdar olarak da karar verebiliyorum.

Örneğin sqlite yerine neden PostgreSQL kullanmalıyım, gibi bir soru sormak beni pek çok konuya yönlendiriyor, gibi gibi.

Çok teşekkür ederim @dildeolupbiten hocam :raised_hands:t2:

1 Beğeni