Apa cara untuk menghindari duplikasi logika antara kelas domain dan kueri SQL?

21

Contoh di bawah ini sepenuhnya buatan dan satu-satunya tujuan adalah untuk menyampaikan maksud saya.

Misalkan saya memiliki tabel SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

Kelas domain:

public class Rectangle {
  private int width;
  private int height;

  /* My business logic */
  public int area() {
    return width * height;
  }
}

Sekarang anggaplah saya memiliki persyaratan untuk menunjukkan kepada pengguna luas total dari semua persegi panjang dalam database. Saya bisa melakukannya dengan mengambil semua baris tabel, mengubahnya menjadi objek dan beralih di atasnya. Tapi ini terlihat bodoh, karena saya punya banyak dan banyak persegi panjang di meja saya.

Jadi saya melakukan ini:

SELECT sum(r.width * r.height)
FROM rectangles r

Ini mudah, cepat dan menggunakan kekuatan basis data. Namun, ia memperkenalkan duplikasi logika, karena saya juga memiliki perhitungan yang sama di kelas domain saya.

Tentu saja, untuk contoh ini duplikasi logika tidak fatal sama sekali. Namun, saya menghadapi masalah yang sama dengan kelas domain saya yang lain, yang lebih kompleks.

Escape Velocity
sumber
1
Saya menduga solusi optimal akan sangat bervariasi dari basis kode ke basis kode, jadi bisakah Anda menjelaskan secara singkat salah satu contoh yang lebih kompleks yang membuat Anda kesulitan?
Ixrec
2
@ lxrec: Laporan. Aplikasi bisnis yang memiliki aturan yang saya tangkap di kelas, dan saya juga perlu membuat laporan yang menunjukkan informasi yang sama, tetapi padat. Perhitungan PPN, pembayaran, pendapatan, hal-hal semacam itu.
Escape Velocity
1
Bukankah ini juga masalah distribusi beban antara server dan klien? Tentu, hanya membuang hasil cache dari perhitungan ke klien adalah taruhan terbaik Anda, tetapi jika data sering berubah dan ada banyak permintaan, mungkin menguntungkan untuk hanya membuang bahan dan resep pada klien alih-alih memasak makanan untuk mereka. Saya pikir itu tidak selalu buruk memiliki lebih dari satu node dalam sistem terdistribusi yang dapat memberikan fungsionalitas tertentu.
null
Saya pikir cara terbaik adalah menghasilkan kode seperti itu. Saya akan jelaskan nanti.
Xavier Combelle

Jawaban:

11

Seperti yang ditunjukkan lxrec, ini akan bervariasi dari basis kode ke basis kode. Beberapa aplikasi akan memungkinkan Anda untuk menempatkan logika bisnis semacam itu ke dalam Fungsi dan / atau kueri SQL dan memungkinkan Anda untuk menjalankannya kapan saja Anda perlu menunjukkan nilai-nilai itu kepada pengguna.

Kadang-kadang mungkin tampak bodoh, tetapi lebih baik untuk kode kebenaran daripada kinerja sebagai tujuan utama.

Dalam sampel Anda, jika Anda menunjukkan nilai area untuk pengguna dalam formulir web, Anda harus:

1) Do a post/get to the server with the values of x and y;
2) The server would have to create a query to the DB Server to run the calculations;
3) The DB server would make the calculations and return;
4) The webserver would return the POST or GET to the user;
5) Final result shown.

Itu bodoh untuk hal-hal sederhana seperti yang ada di sampel, tetapi mungkin perlu untuk hal-hal yang lebih kompleks seperti menghitung IRR dari investasi klien dalam sistem perbankan.

Kode untuk kebenaran . Jika perangkat lunak Anda benar, tetapi lambat, Anda akan memiliki peluang untuk mengoptimalkan di tempat yang Anda butuhkan (setelah profil). Jika itu berarti menyimpan beberapa logika bisnis dalam database, biarlah. Itu sebabnya kami memiliki teknik refactoring.

Jika menjadi lambat, atau tidak responsif, Anda mungkin harus melakukan beberapa optimasi, seperti melanggar prinsip KERING, yang bukan merupakan dosa jika Anda mengelilingi diri Anda dari pengujian unit yang tepat dan pengujian konsistensi.

Machado
sumber
1
Masalah dengan menempatkan logika bisnis (prosedural) dalam SQL adalah hal yang sangat menyakitkan bagi refactor. Bahkan jika Anda memiliki alat refactoring SQL kedudukan tertinggi, mereka biasanya tidak berinteraksi dengan alat refactoring kode di IDE Anda (atau setidaknya saya belum melihat toolset seperti itu)
Roland Tepp
2

Anda mengatakan bahwa contohnya adalah buatan, jadi saya tidak tahu apakah yang saya katakan di sini sesuai dengan situasi Anda yang sebenarnya, tetapi jawaban saya adalah - gunakan lapisan ORM (Object-relational mapping) untuk menentukan struktur dan kueri / manipulasi dari database Anda. Dengan begitu Anda tidak memiliki logika duplikat, karena semuanya akan ditentukan dalam model.

Misalnya, menggunakan kerangka Django (python), Anda akan mendefinisikan kelas domain persegi panjang Anda sebagai model berikut :

class Rectangle(models.Model):
    width = models.IntegerField()
    height = models.IntegerField()

    def area(self):
        return self.width * self.height

Untuk menghitung total area (tanpa pemfilteran) Anda akan mendefinisikan:

def total_area():
    return sum(rect.area() for rect in Rectangle.objects.all())

Seperti yang disebutkan orang lain, Anda harus terlebih dahulu kode untuk kebenaran, dan hanya mengoptimalkan ketika Anda benar-benar menemui hambatan. Jadi, jika di kemudian hari Anda memutuskan, Anda benar-benar harus mengoptimalkan, Anda bisa beralih ke mendefinisikan kueri mentah, seperti:

def total_area_optimized():
    return Rectangle.objects.raw(
        'select sum(width * height) from myapp_rectangle')
yoniLavi
sumber
1

Saya telah menulis contoh konyol untuk menjelaskan sebuah ide:

class BinaryIntegerOperation
{
    public int Execute(string operation, int operand1, int operand2)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            var args = split[1].Split(',');
            var result = IsFirstOperand(args[0]) ? operand1 : operand2;
            for (var i = 1; i < args.Length; i++)
            {
                result *= IsFirstOperand(args[i]) ? operand1 : operand2;
            }
            return result;
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    public string ToSqlExpression(string operation, string operand1Name, string operand2Name)
    {
        var split = operation.Split(':');
        var opCode = split[0];
        if (opCode == "MULTIPLY")
        {
            return string.Join("*", split[1].Split(',').Select(a => IsFirstOperand(a) ? operand1Name : operand2Name));
        }
        else
        {
            throw new NotImplementedException();
        }
    }
    private bool IsFirstOperand(string code)
    {
        return code == "0";
    }
}

Jadi, jika Anda memiliki beberapa logika:

var logic = "MULTIPLY:0,1";

Anda dapat menggunakannya kembali di kelas domain:

var op = new BinaryIntegerOperation();
Console.WriteLine(op.Execute(logic, 3, 6));

Atau di lapisan sql-generation Anda:

Console.WriteLine(op.ToSqlExpression(logic, "r.width", "r.height"));

Dan, tentu saja, Anda dapat mengubahnya dengan mudah. Coba ini:

logic = "MULTIPLY:0,1,1,1";
astef
sumber
-1

Seperti yang dikatakan @Machado, cara termudah untuk melakukannya adalah menghindarinya dan melakukan semua pemrosesan Anda di java utama Anda. Namun, masih mungkin untuk memiliki basis kode dengan kode yang sama tanpa mengulangi diri Anda dengan membuat kode untuk kedua basis kode.

Misalnya menggunakan roda gigi memungkinkan untuk menghasilkan tiga cuplikan dari definisi umum

cuplikan 1:

/*[[[cog
from generate import generate_sql_table
cog.outl(generate_sql_table("rectangle"))
]]]*/
CREATE TABLE rectangles (
    width int,
    height int
);
/*[[[end]]]*/

cuplikan 2:

public class Rectangle {
    /*[[[cog
      from generate import generate_domain_attributes,generate_domain_logic
      cog.outl(generate_domain_attributes("rectangle"))
      cog.outl(generate_domain_logic("rectangle"))
      ]]]*/
    private int width;
    private int height;
    public int area {
        return width * heigh;
    }
    /*[[[end]]]*/
}

cuplikan 3:

/*[[[cog
from generate import generate_sql
cog.outl(generate_sql("rectangle","""
                       SELECT sum({area})
                       FROM rectangles r"""))
]]]*/
SELECT sum((r.width * r.heigh))
FROM rectangles r
/*[[[end]]]*/

dari satu file referensi

import textwrap
import pprint

# the common definition 

types = {"rectangle":
    {"sql_table_name": "rectangles",
     "sql_alias": "r",
     "attributes": [
         ["width", "int"],
         ["height", "int"],
     ],
    "methods": [
        ["area","int","this.width * this.heigh"],
    ]
    }
 }

# the utilities functions

def generate_sql_table(name):
    type = types[name]
    attributes =",\n    ".join("{attr_name} {attr_type}".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])
    return """
CREATE TABLE {table_name} (
    {attributes}
);""".format(
    table_name=type["sql_table_name"],
    attributes = attributes
).lstrip("\n")


def generate_method(method_def):
    name,type,value =method_def
    value = value.replace("this.","")
    return textwrap.dedent("""
    public %(type)s %(name)s {
        return %(value)s;
    }""".lstrip("\n"))% {"name":name,"type":type,"value":value}


def generate_sql_method(type,method_def):
    name,_,value =method_def
    value = value.replace("this.",type["sql_alias"]+".")
    return name,"""(%(value)s)"""% {"value":value}

def generate_domain_logic(name):
    type = types[name]
    attributes ="\n".join(generate_method(method_def)
                   for method_def
                   in type["methods"])

    return attributes


def generate_domain_attributes(name):
    type = types[name]
    attributes ="\n".join("private {attr_type} {attr_name};".format(
        attr_name=attr_name,
        attr_type=attr_type)
                   for (attr_name,attr_type)
                   in type["attributes"])

    return attributes

def generate_sql(name,sql):
    type = types[name]
    fields ={name:value
             for name,value in
             (generate_sql_method(type,method_def)
              for method_def in type["methods"])}
    sql=textwrap.dedent(sql.lstrip("\n"))
    print (sql)
    return sql.format(**fields)
Xavier Combelle
sumber