วิธีการหลีกเลี่ยงการทำซ้ำตรรกะระหว่างคลาสโดเมนและแบบสอบถาม SQL คืออะไร


21

ตัวอย่างด้านล่างเป็นสิ่งประดิษฐ์โดยสิ้นเชิงและจุดประสงค์เพียงอย่างเดียวคือเพื่อให้ได้จุดของฉัน

สมมติว่าฉันมีตาราง SQL:

CREATE TABLE rectangles (
  width int,
  height int 
);

ระดับโดเมน:

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

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

ตอนนี้สมมติว่าฉันมีความต้องการที่จะแสดงให้ผู้ใช้เห็นพื้นที่ทั้งหมดของรูปสี่เหลี่ยมผืนผ้าทั้งหมดในฐานข้อมูล ฉันสามารถทำได้โดยดึงแถวทั้งหมดของตารางเปลี่ยนเป็นวัตถุและวนซ้ำไปมา แต่มันดูโง่มากเพราะฉันมีรูปสี่เหลี่ยมผืนผ้ามากมายในตาราง

ดังนั้นฉันทำสิ่งนี้:

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

ง่ายรวดเร็วและใช้จุดแข็งของฐานข้อมูล อย่างไรก็ตามมันแนะนำตรรกะที่ซ้ำกันเพราะฉันมีการคำนวณในชั้นโดเมนของฉันด้วย

แน่นอนว่าสำหรับตัวอย่างนี้การทำซ้ำตรรกะไม่ร้ายแรงเลย อย่างไรก็ตามฉันประสบปัญหาเดียวกันกับคลาสโดเมนอื่นที่ซับซ้อนกว่า


1
ฉันสงสัยว่าทางออกที่ดีที่สุดจะแตกต่างกันอย่างมากจาก codebase ไปเป็น codebase ดังนั้นคุณสามารถอธิบายสั้น ๆ หนึ่งในตัวอย่างที่ซับซ้อนมากขึ้นที่ทำให้คุณมีปัญหาได้หรือไม่?
Ixrec

2
@lxrec: รายงาน แอปพลิเคชันธุรกิจที่มีกฎที่ฉันจับภาพในชั้นเรียนและฉันต้องสร้างรายงานที่แสดงข้อมูลเดียวกัน แต่ย่อ การคำนวณ VAT การชำระเงินรายได้สิ่งของประเภทนั้น
Escape Velocity

1
นี่ไม่ใช่คำถามของการกระจายโหลดระหว่างเซิร์ฟเวอร์และลูกค้าหรือไม่ แน่นอนว่าการทิ้งผลลัพธ์ที่แคชไว้ในการคำนวณให้กับลูกค้าเป็นทางออกที่ดีที่สุดของคุณ แต่หากข้อมูลมีการเปลี่ยนแปลงบ่อยครั้งและมีคำขอจำนวนมากอาจเป็นประโยชน์ที่จะสามารถโยนส่วนผสมและสูตรที่ลูกค้าแทน ทำอาหารสำหรับพวกเขา ฉันคิดว่ามันไม่จำเป็นต้องเป็นเรื่องเลวร้ายที่จะมีมากกว่าหนึ่งโหนดในระบบแบบกระจายที่สามารถให้การทำงานบางอย่าง
null

ฉันคิดว่าวิธีที่ดีที่สุดคือการสร้างรหัสดังกล่าว ฉันจะอธิบายในภายหลัง
ซาเวียร์ Combelle

คำตอบ:


11

เมื่อ lxrec ชี้ให้เห็นมันจะแตกต่างจาก codebase เป็น codebase แอปพลิเคชั่นบางตัวจะอนุญาตให้คุณใส่ตรรกะทางธุรกิจประเภทนั้นลงในฟังก์ชัน SQL และ / หรือแบบสอบถามและอนุญาตให้คุณเรียกใช้แอปเหล่านั้นได้ทุกเวลาที่คุณต้องการแสดงค่าเหล่านั้นให้กับผู้ใช้

บางครั้งมันอาจดูโง่ แต่ควรเขียนโค้ดให้ถูกต้องดีกว่าประสิทธิภาพเป็นวัตถุประสงค์หลัก

ในตัวอย่างของคุณหากคุณแสดงมูลค่าของพื้นที่สำหรับผู้ใช้ในเว็บฟอร์มคุณจะต้อง:

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.

มันโง่สำหรับสิ่งง่าย ๆ อย่างตัวอย่างในตัวอย่าง แต่อาจจำเป็นต้องมีสิ่งที่ซับซ้อนกว่าเช่นการคำนวณ IRR ของการลงทุนของลูกค้าในระบบธนาคาร

สำหรับความถูกต้อง หากซอฟต์แวร์ของคุณถูกต้อง แต่ช้าคุณจะมีโอกาสที่จะปรับตำแหน่งที่คุณต้องการ (หลังจากทำโปรไฟล์) ถ้านั่นหมายถึงการรักษาตรรกะทางธุรกิจบางอย่างไว้ในฐานข้อมูล นั่นเป็นเหตุผลที่เรามีเทคนิคการสร้างใหม่

หากมันช้าหรือไม่ตอบสนองมากกว่าที่คุณอาจมีการเพิ่มประสิทธิภาพที่จะทำเช่นการละเมิดหลักการ DRY ซึ่งไม่ได้เป็นบาปถ้าคุณล้อมรอบตัวเองของการทดสอบหน่วยที่เหมาะสมและการทดสอบความสอดคล้อง


1
ปัญหาเกี่ยวกับการวางตรรกะทางธุรกิจ (ขั้นตอน) ใน SQL นั้นเป็นสิ่งที่เจ็บปวดอย่างยิ่งที่จะทำการปรับเปลี่ยนใหม่ แม้ว่าคุณจะมีเครื่องมือการ refactoring SQL บนรอยพวกเขามักจะไม่เชื่อมต่อกับเครื่องมือการ refactoring รหัสใน IDE ของคุณ (หรืออย่างน้อยฉันยังไม่ได้เห็นชุดเครื่องมือดังกล่าว)
Roland Tepp

2

คุณบอกว่าตัวอย่างเป็นสิ่งประดิษฐ์ดังนั้นฉันไม่รู้ว่าสิ่งที่ฉันกำลังพูดอยู่ที่นี่เหมาะกับสถานการณ์จริงของคุณหรือไม่ แต่คำตอบของฉันคือ - ใช้เลเยอร์ORM (การทำแผนที่วัตถุสัมพันธ์) เพื่อกำหนดโครงสร้างและสอบถาม / จัดการ ฐานข้อมูลของคุณ ด้วยวิธีนี้คุณไม่มีตรรกะที่ซ้ำซ้อนเนื่องจากทุกอย่างจะถูกกำหนดในแบบจำลอง

ตัวอย่างเช่นการใช้Django (python) เฟรมเวิร์กคุณจะกำหนดคลาสโดเมนสี่เหลี่ยมผืนผ้าของคุณเป็นโมเดลต่อไปนี้:

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

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

ในการคำนวณพื้นที่ทั้งหมด (โดยไม่มีการกรองใด ๆ ) คุณต้องกำหนด:

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

ดังที่คนอื่น ๆ พูดถึงคุณควรเขียนโค้ดให้ถูกต้องก่อนและปรับให้เหมาะสมเมื่อคุณพบปัญหาคอขวด ดังนั้นหากคุณตัดสินใจในภายหลังคุณจะต้องปรับให้เหมาะสมอย่างแน่นอนคุณสามารถเปลี่ยนไปใช้การกำหนดคิวรีแบบดิบเช่น:

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

1

ฉันได้เขียนตัวอย่างโง่ ๆ เพื่ออธิบายแนวคิด:

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

ดังนั้นถ้าคุณมีเหตุผลบางอย่าง:

var logic = "MULTIPLY:0,1";

คุณสามารถใช้ซ้ำได้ในคลาสโดเมน:

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

หรือในเลเยอร์ sql-generation ของคุณ:

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

และแน่นอนคุณสามารถเปลี่ยนได้อย่างง่ายดาย ลองสิ่งนี้:

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

-1

วิธีที่ง่ายที่สุดที่จะทำคือการหลีกเลี่ยงและทำการประมวลผลทั้งหมดของคุณในจาวาหลักของคุณ อย่างไรก็ตามยังคงเป็นไปได้ที่จะต้องใช้รหัสฐานกับรหัสที่คล้ายกันโดยไม่ต้องทำซ้ำตัวเองโดยสร้างรหัสสำหรับรหัสฐานทั้งสอง

ตัวอย่างเช่นการใช้ฟันเฟืองเปิดใช้งานเพื่อสร้างตัวอย่างสามจากคำจำกัดความทั่วไป

ตัวอย่าง 1:

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

ตัวอย่าง 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]]]*/
}

ตัวอย่าง 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]]]*/

จากไฟล์อ้างอิงหนึ่งไฟล์

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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.