Etki alanı sınıfları ve SQL sorguları arasında mantığın tekrarlanmasını önlemenin yolları nelerdir?


21

Aşağıdaki örnek tamamen yapay ve tek amacı noktaya ulaşmak.

Bir SQL tablosu olduğunu varsayalım:

CREATE TABLE rectangles (
  width int,
  height int 
);

Etki alanı sınıfı:

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

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

Şimdi, kullanıcıya veritabanındaki tüm dikdörtgenlerin toplam alanını gösterme gereksinimi olduğunu varsayalım. Bunu tablonun tüm satırlarını alıp, nesneleri nesnelere çevirerek ve üzerinde yineleyerek yapabilirim. Ama bu sadece aptalca görünüyor, çünkü masamda çok ve çok sayıda dikdörtgen var.

Bu yüzden bunu yapıyorum:

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

Bu kolay, hızlı ve veritabanının gücünü kullanır. Ancak, kopyalanan mantığı da tanıtıyor, çünkü aynı zamanda domain sınıfımda da aynı hesaplamaya sahibim.

Tabii ki, bu örnek için mantığın çoğaltılması hiç de önemli değil. Bununla birlikte, aynı problemi daha karmaşık olan diğer alan sınıflarımla da yaşıyorum.


1
En uygun çözümün kod temeli ile kod temeli arasında oldukça çılgınca değişeceğinden şüpheliyim, bu yüzden size sorun çıkaran daha karmaşık örneklerden birini kısaca açıklayabilir misiniz?
Ixrec,

2
@lxrec: Raporlar. Sınıfta yakaladığım kuralları olan bir iş uygulaması ve aynı bilgileri gösteren ancak yoğunlaştırılmış raporlar da oluşturmam gerekiyor. KDV hesaplamaları, ödemeler, kazanç, böyle şeyler.
Kaçış Hızı

1
Bu aynı zamanda yükü sunucu ve istemciler arasında dağıtma meselesi değil midir? Tabii ki, hesaplamanın önbelleğe alınmış sonucunu bir müşteriye atmak en iyisidir, ancak veriler sık ​​sık değişirse ve çok sayıda istek varsa, içeriği yerine müşteriye sadece malzemeyi ve reçeteyi atabilmek avantajlı olabilir. onlar için yemek yapmayı. Belirli bir işlevsellik sağlayabilen dağıtılmış bir sistemde birden fazla düğüme sahip olmanın mutlaka kötü bir şey olmadığını düşünüyorum.
null,

Bence en iyi yol bu tür kodları oluşturmak. Sonra açıklayacağım.
Xavier Combelle

Yanıtlar:


11

Lxrec'in işaret ettiği gibi, kod tabanından kod tabanına kadar değişecektir. Bazı uygulamalar, bu tür iş mantığını SQL İşlevlerine ve / veya sorgulara koymanıza ve bu değerleri kullanıcıya göstermeniz gerektiğinde bunları çalıştırmanıza izin verir.

Bazen aptal görünebilir, ancak doğruluğu kodlamak, birincil amaç olarak performanstan daha iyidir.

Örnekte, bir kullanıcı için alanın değerini web biçiminde gösteriyorsanız, şunları yapmanız gerekir:

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.

Örnekteki gibi basit şeyler için aptalca, ancak bir müşterinin bir bankacılık sistemine yaptığı yatırımın IRR'sini hesaplamak gibi daha karmaşık şeyler için gerekli olabilir.

Doğruluk kodu . Yazılımınız doğruysa, ancak yavaşsa, ihtiyacınız olan yeri optimize etme şansınız (profilden sonra). Bu, iş mantığının bir kısmını veritabanında tutmak anlamına gelirse, öyle olsun. Bu yüzden refactoring tekniklerimiz var.

Eğer yavaşlarsa veya cevap vermiyorsa, DRY prensibini ihlal etmek gibi bazı optimizasyonlara sahip olabilirsiniz, bu da kendinizi uygun ünite testini ve tutarlılık testini kuşatırsanız günah değildir.


1
SQL'de (prosedürel) iş mantığını ortaya koymadaki sorun, refactor için son derece acı vericidir. Birinci sınıf SQL yeniden düzenleme araçlarına sahip olsanız bile, genellikle IDE'nizde kod yeniden düzenleme araçlarıyla arayüz oluşturmazlar (veya en azından henüz böyle bir araç seti görmedim)
Roland Tepp

2

Örneğin yapay olduğunu söylüyorsunuz, bu yüzden burada söylediklerimin gerçek durumunuza uyup uymadığını bilmiyorum, ama cevabım - yapısını ve sorgusunu / manipülasyonunu tanımlamak için bir ORM (Nesne-ilişkisel haritalama) katmanı kullanın. Veritabanınız Bu şekilde kopyalanmış bir mantığınız yoktur, çünkü her şey modellerde tanımlanacaktır.

Örneğin, Django (python) çerçevesini kullanarak, dikdörtgen alan sınıfınızı aşağıdaki model olarak tanımlarsınız :

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

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

Toplam alanı hesaplamak için (filtreleme olmadan) tanımlayın:

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

Diğerlerinin de belirttiği gibi, önce doğruluk için kod yazmalı ve yalnızca gerçekten bir tıkanıklığa çarptığınızda optimize etmelisiniz. Bu nedenle, daha sonraki bir tarihte karar verirseniz, kesinlikle en iyi duruma getirmeniz gerekiyorsa, aşağıdaki gibi bir ham sorgu tanımlamaya geçebilirsiniz:

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

1

Bir fikri açıklamak için aptal bir örnek yazdım:

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";
    }
}

Yani, eğer bir mantığınız varsa:

var logic = "MULTIPLY:0,1";

Etki alanı sınıflarında yeniden kullanabilirsiniz:

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

Veya sql oluşturma katmanınızda:

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

Ve tabii ki kolayca değiştirebilirsiniz. Bunu dene:

logic = "MULTIPLY:0,1,1,1";

-1

@ Machado'nun dediği gibi, bunu yapmanın en kolay yolu onu önlemek ve tüm işlemlerinizi ana java'da yapmaktır. Bununla birlikte, her iki kod tabanı için bir kod oluşturarak kendi kodunuzu kendiniz tekrarlamadan, benzer kodla temel vermek zorunda kalırsınız.

Örneğin , ortak bir tanımdan üç parçacığı oluşturmak için cog enable özelliğini kullanma

snippet 1:

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

pasaj 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]]]*/
}

snippet 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]]]*/

bir referans dosyasından

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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.