Transaction Script to Active record

Posted on:December 28, 2022

Transaction Script และ Active record เขียนและอธิบายไว้อย่างละเอียดในหนังสือ Patterns of Enterprise Application Architecture โดย Martin Folwer เนื้อหาในบทความนี้เกิน 150% จะอ้างอิงจากหนังสือ

Transaction Script

คือวิธีการพัฒนา application ด้วยแนวคิดแบบ domain logic pattern เหมาะกับระบบขนาดเล็กและมีรูปแบบของธุรกิจที่ไม่ซับซ้อนจุดขายของ pattern คือความเรียบง่าย เราไม่ต้องปวดหัวกับการออกแบบ architecture การตั้งชื่อของ handler จะอ้างอิงถึง transaction ที่ดูแล เมื่อมี request เข้ามา คำสั่งในการดึงข้อมูล เพิ่ม ลบ อัปเดต และ business logic ทั้งหมดจะทำงานและจบใน procedure handles a single ตัวอย่างเช่น ถ้าเราทำระบบซื้อไข่ไก่ออนไลน์ เมื่อลูกค้าสั่งซื้อไข่ไก่ การตรวจเช็คจำนวนไข่ไก่ หลังสั่งซื้อสำเร็จใข่ไก่จะเหลือกี่ฟอง ระบบต้องเก็บข้อมูลอะไรในใบสั่งซื้อบ้าง เงื่อนไขเหล่านี้จะอยู่ในขั้นตอน สั่งซื้อไข่ไก่ (PlaceOrder)

ตัวอย่างโค้ด สั่งซื้อไข่ไก่ เราสามารถมาแยก transaction ออกไปเป็น functions ย่อยๆ ได้ นั่นช่วยให้เราสามารถทำความเข้าใจโค้ดได้ง่ายขึ้น

public void PlaceOrder(IPlaceOrder order)
    {
        var db = new NpgsqlConnection(_connString);
        var stock = GetStockByProductID(order.ProductId);
        if (stock.Quantity < order.Quantity)
        {
            throw new Exception("Not enough stock.");
        };

        db.Open();
        var transaction = db.BeginTransaction();
        try
        {
            UpdateStock(db.CreateCommand(), stock.ID, stock.Quantity - order.Quantity);
            CreateOrder(db.CreateCommand(), order.CustomerId, order.ProductId, order.Quantity);
            transaction.Commit();
        }
        catch (System.Exception)
        {
            transaction.Rollback();
            throw;
        }

    }

    private void CreateOrder(NpgsqlCommand command, int customerId, int productId, int quantity)
    {
        command.CommandText = "insert into orders (customer_id, product_id, quantity) values (@customer_id, @product_id, @quantity)";
        command.Parameters.AddWithValue("@customer_id", customerId);
        command.Parameters.AddWithValue("@product_id", productId);
        command.Parameters.AddWithValue("@quantity", quantity);
        command.ExecuteNonQuery();
    }

    private void UpdateStock(NpgsqlCommand command, int id,int quantity)
    {
        command.CommandText = "update stock set quantity = @quantity where id = @id";
        command.Parameters.AddWithValue("@quantity", quantity);
        command.Parameters.AddWithValue("@id", id);
        command.ExecuteNonQuery();
    }

 เนื่องจาก transaction ต้องแก้ไข ลบและเพิ่มข้อมูลจากหลายๆ ตาราง พร้อมกัน ดังนั้น เราต้องมั่นใจว่าเมื่อเกิดข้อผิดพลาด เช่น server executing, network มีปัญหา, database timeout/deadlock หรือข้อผิดพลาดอะไรก็ช่าง จะไม่ส่งผลกระทบกับข้อมูล ไม่ควรมีการสร้างข้อมูล order แต่จำนวนไข่ไก่ใน stock ไม่ลดลง จนเกิดปัญหาไม่มีไข่ไก่ส่งให้ลูกค้า เพราะว่าระบบอัปเดตข้อมูล stock ไม่สำเร็จ (คำสั่งในการ Rollback Transaction โดยส่วนมากจะมีมาให้กับ relational database เเทบจะทุกตัว)  ความยากของ transaction script คือการ maintenance source code เมื่อระบบเริ่มใหญ่ เราจะเริ่มเห็น duplication code ระหว่าง transaction ถ้าเราเริ่มได้กลิ่น นี่อาจจะเป็นสัญญาณที่บอกว่าเราว่าควรเริ่มปรับเปลี่ยนวิธี implementing domain logic

Active record

An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data

Martin Fowler

Martin Fowler ให้นิยามเกี่ยวกับ active record ว่าเป็นการสร้าง object ที่หน้าตา ตรงกับตารางในฐานข้อมูล หมายความว่า 1 ฟิลด์ใน object คือ 1 คอลัมน์ในตาราง และเราจะย้าย business logic ที่เกี่ยวข้องกับข้อมูลเข้าไปใน object เช่น วิธีเช็คจำนวนไข่ไก่หรือ reserve เพื่อลดจำนวนไข่ไก่ใน stock active-record

Active Record เป็นตัวเลือกที่ดีสำหรับคนที่ใช้ transaction script และเริ่มเจอปัญหา เงื่อนไขในการเช็คข้อมูลกระจัดกระจายตาม handler หรือการจัดการ state ของ transaction เริ่มยุ่งยาก เพราะต้องใช้ข้อมูลจากหลายๆ ตาราง เราสามารถค่อยๆ refactor ส่วนของ behavior ที่เป็น business logic ย้ายไปเก็บที่ active record ได้ ตัวอย่างโค้ดที่แปลงจาก Transaction Script to Active Record

public void PlaceOrder(IPlaceOrder order)
    {
        var db = new DBStockKeepingContext();
        var transaction = db.Database.BeginTransaction();
        var stock = GetStockByProductID(order.ProductId);
        if (stock.isReadyForShipping(order.Quantity) == false) {
            throw new Exception("Not enough stock.");
        };
        try
        {
            db.Order.Add(new Order
            {
                CustomerID = order.CustomerId,
                ProductID = order.ProductId,
                Quantity = order.Quantity
            });
            stock.reserve(order.Quantity);
            db.SaveChanges();
            transaction.Commit();
        }
        catch (System.Exception)
        {
            transaction.Rollback();
            throw;
        }
    }

 ใช่ครับ เรายังขายไข่ไก่เหมือนเดิม สิ่งที่เพิ่มเติมคือ business logic ในการตรวจว่าเหลือไข่ไก่พอสำหรับคำสั่งซื้อไหมหรือซื้อไปแล้วใน stock เหลือเท่าไรจะถูกย้ายไปที่ object แทน


 ข้อแตกต่างที่เห็นได้ชัดคือ แทนที่จะเข้าถึงฐานข้อมูลโดยตรง เราจะจัดการผ่าน object แทน โดยที่ record แต่ละรายการมีหน้าที่รับผิดชอบ แก้ไข ลบ และสร้างพร้อมกับบันทึกไปยังฐานข้อมูล เราสามารถอ้างอิงถึง data structures และใส่ behaviors ให้กับ object ผ่านชื่อ functions ข้อควรระวังคือเรื่อง object ที่มีหน้าตาเหมือน database schema เพราะมีโอกาสที่จะเปลี่ยนแปลงสูงมาก หมายความว่าระบบเราอาจจะมีบัคเนื่องจากมีการเพิ่ม field ในตารางฐานข้อมูล หรือ ปัญหาในการเขียนเทส ถ้า object นั่นมี dependecy กับ object อื่นๆ


active record ถูกมองว่า anti pattern? บางคนมองว่าผิด rules and best practices of OOP หรือ เกิดความสับสนระหว่าง active record หรือ objects หรือ data structures แต่ก็มีการเอา active record ไปใช้ร่วมกับ ORM หรือ data access to database หลายๆ ตัว สิ่งนี้คนใช้ต้องเป็นคนตัดสินใจเอง.


Active Record ทำงานได้ดีกับโดเมนที่ไม่ซับซ้อน เช่น สร้าง อ่าน อัปเดต ลบและทำงาน based on a single record ถ้างานที่เราทำเริ่มมีความซับซ้อนมากขึ้น และเราต้องการอ้างอิงถึงข้อมูลหลายๆ ตางรางเพื่อให้ตรงไปตามที่ธุรกิจต้องการ สิ่งนี้ทำให้เราต้องมี business logic ใน handler และ object ผสมผสานจนเป็นโกปิโก้  ในบทความต่อไปเราจะมารู้จัก implementing domain logic ที่ชื่อว่า Domain Model