Làm việc với Disconnected Data – DataSet và SqlDataAdapter

Lesson 05 hướng dẫn cách làm việc với dữ liệu ngắt kết nối (disconnected data) với DataSet và SqlDataAdapter.

Giới thiệu

Một DataSet là một đối tượng chứa dữ liệu trong bộ nhớ và có thể gồm nhiều bảng. DataSet chỉ chứa dữ liệu chứ không tương tác với nguồn dữ liệu. Thay vào đó, SqlDataAdapter sẽ được dùng để quản lý các kết nối với nguồn dữ liệu và cho chúng ta chế độ làm việc disconnected. SqlDataAdapter mở  một kết nối chỉ khi cần thiết và đóng nó ngay sau khi tác vụ được hoàn thành. Ví dụ, SqlDataAdapter thực hiện các tác vụ sau, khi đổ dữ liệu vào DataSet:

1. Mở kết nối

2. Đổ dữ liệu vào DataSet (Fill)

3. Đóng kết nối

Và thực hiện các công việc sau, khi cập nhật dữ liệu nguồn với thay đổi của DataSet:

1. Mở kết nối

2. Ghi thay đổi từ DataSet vào dữ liệu nguồn (Update)

3. Đóng kết nối

Giữa hai thao tác Fill và Update, các kết nối với nguồn dữ liệu được đóng lại và bạn có thể tự do ghi, đọc dữ liệu với DataSet. Đây chính là cơ chế của mô hình làm việc với disconnected data. Bởi vì ứng dụng sẵn sàng kết nối khi cần thiết, ứng dụng trở nên dễ phát triển hơn.


Hai kịch bản minh họa cho lý do tại sao bạn cần làm việc với disconnected data: người dùng làm việc không cần kết nối mạng và giúp Web site dễ phát triển hơn. Hãy xem xét việc nhà kinh doanh cần dữ liệu của khách hàng trong khi họ đi công tác. Khi bắt đầu ngày, họ sẽ cần đồng bộ dữ liệu với database chính  để lấy các thông tin cập nhật mới nhất. Trong suốt ngày hôm đó, họ sẽ thực hiện các thay đổi trên dữ liệu khách hàng hiện tại, thêm khách hàng mới, và nhập các hóa đơn mới. Điều này hợp lệ vì họ có nguồn dữ liệu khách hàng riêng và những người khác không thể thay đổi trên cùng dòng dữ liệu. Vào cuối ngày, nhà kinh doanh cập nhật sẽ kết nối vào mạng và cập nhật thay đổi cho tiến trình xử lý ban đêm.

Kịch bản khác là giúp Web site dễ phát triển hơn.Với một SqlDataReader, bạn phải trở lại database lấy dữ liệu mỗi khi xem một trang. Điều này yêu cầu một kết nối mới cho mỗi lần tải trang, nó sẽ ảnh hưởng lớn đến hiệu suất khi số lượng người dùng tăng lên. Một cách để khắc phục điều này là dùng DataSet, chỉ cần cập nhật một lần và lưu trong bộ nhớ tạm (cache). Mỗi yêu cầu tải trang sẽ kiểm tra cache và nạp dữ liệu (từ database) nếu nó không tồn tại hoặc lấy dữ liệu ra khỏi cache và hiển thị nó. Điều này giúp hạn chế truy xuất database và tăng hiệu suất cho ứng dụng của bạn.

Các trường hợp ngoại lệ cho kịch bản trên bao gồm các trường hợp bạn cần cập nhật dữ liệu. Bạn phải quyết định, dựa trên cách dữ liệu sẽ được dùng và chiến lược của bạn. Dùng disconnected data khi thông tin của bạn thường là read-only, nhưng hãy xem xét các giải pháp thay thế (như dùng đối tượng SqlCommand để cập nhật tức thời) khi bạn yêu cầu một vài thứ linh hoạt hơn. Cũng vậy, nếu số lượng dữ liệu quá lớn để lưu trong bộ nhớ, bạn sẽ cần dùng SqlDataReader để đọc dữ liệu trong C#.

Tạo đối tượng DataSet

Không có gì đặc biệt khi tạo một DataSet. Bạn chỉ cần tạo một thể hiện mới, giống bất kì đối tượng nào:

DataSet dsCustomers = new DataSet();

Constructor của DataSet không yêu cầu tham số. Tuy nhiên có một overload chấp nhận một chuỗi đại diện cho tên của DataSet, được dùng nếu bạn cần serialize dữ liệu thành XML. Bây giờ, ta có một DataSet rỗng và cần một SqlDataAdapter để nạp dữ liệu cho nó.

Tạo một SqlDataAdapter

SqlDataAdapter chứa các lệnh SQL và đối tượng connection để đọc và ghi dữ liệu. Bạn khởi tạo nó với câu SQL select và đối tượng connection:

SqlDataAdapter daCustomers = new SqlDataAdapter(“select CustomerID, CompanyName from Customers”, conn);

Dòng mã trên tạo một đối tượng SqlDataAdapter, daCustomers. Câu SQL select xác định dữ liệu nào sẽ được đọc vào DataSet. Đối tượng connection, conn, nên được khởi tạo từ trước, nhưng không được mở. Đó là công việc của SqlDataAdapter để mở và đóng connection khi phương thức Fill() và Update() được gọi.

SqlDataAdapter tất cả lệnh cần thiết để tương tác với dữ liệu nguồn. Dòng mã trên xác định câu lệnh select, nhưng không cho thấy câu lệnh insert, update và delete. Chúng sẽ được thêm vào SqlDataAdapter sau khi nó được khởi tạo.

Có hai cách để thêm các lệnh insert, update, delete: thông qua các property của SqlDataAdapter hoặc với một SqlCommandBuilder. Trong bài này, tôi sẽ cho bạn thấy cách dễ nhất để làm điều này với SqlCommandBuilder. Trong bài sau, tôi sẽ sử dụng các property của SqlDataAdapter, công việc này sẽ đòi hỏi nhiều bước hơn nhưng sẽ giúp bạn hiệu quả hơn cách làm việc của SqlCommandBuilder. Đây là cách để thêm các lệnh vào SqlDataAdapter với SqlCommandBuilder.

SqlCommandBuilder cmdBldr = new SqlCommandBuilder(daCustomers);

Lưu ý rằng  dòng mã trên khởi tạo một đối tượng SqlCommandBuilder với constructor cần một tham số là SqlDataAdapter, daCustomers. Điều này giúp SqlCommandBuilder biết đối tượng SqlDataAdapter nào để thêm các lệnh vào. SqlCommandBuilder sẽ đọc câu SQL select (lấy từ SqlDataAdapter), từ đó suy ra các lệnh insert, update và delete, sau đó gán các lệnh mới vào các property Insert, Update, Delete của SqlDataAdapter tương ứng.

Như tôi đề cập từ trước, SqlCommandBuilder có hạn chế. Nó làm việc khi bạn làm một câu select đơn giản trên một bảng. Tuy nhiên, khi bạn cần kết hợp hai hoặc nhiều bảng hoặc thực thi một stored procedure, nó sẽ không làm việc.

Đổ dữ liệu vào DataSet

Để đổ dữ liệu vào DataSet bạn cần dùng phương thức Fill() của SqlDataAdapter, như sau:

daCustomers.Fill(dsCustomers, “Customers”);

Phương thức Fill(), trong dòng trên lấy hai tham số: một DataSet và một tên bảng.  DataSet phải được tạo trước khi bạn đổ dữ liệu vào nó. Tham số thứ hai là tên của bảng sẽ được tạo trong DataSet. Bạn có thể đặt bất kì tên gì cho bảng. Thông thường, tôi sẽ để tên bảng trùng với tên gốc của nó trong database. Tuy nhiên, nếu câu select của SqlDataAdapter chứa một lệnh join, bạn sẽ cần phải đặt một tên rõ ràng khác cho bảng.

Phương thức Fill() có một overload chấp nhận một tham số là DataSet. Trong trường hợp này, bảng được tạo sẽ có tên mặc định là “table1” cho bảng đầu tiên. Số này sẽ tăng dần (table2, tabl3,…,tableN) cho mỗi bảng thêm vào DataSet nếu như tên bảng không được chỉ ra trong phương thức Fill().

Sử dụng DataSet

Một DataSet sẽ gắn dữ liệu vào DataGrid của ASP.NET và Windows form. Đây là một ví dụ sẽ gán DataSet cho một Windows forms DataGrid:

dgCustomers.DataSource = dsCustomers;

dgCustomers.DataMember = “Customers”;

Điều đầu tiên chúng ta làm, trong đoạn mã trên, là gán DataSet cho property DataSource của DataGrid. Điều này giúp DataGrid biết nó có dữ liệu được gắn vào, tuy nhiên bạn sẽ thấy một dấu ‘+’ trên GUI bởi vì DataSet có thể giữ nhiều bảng và DataGrid cho phép bạn mở rộng ra để xem mỗi bảng trong đó. Để chỉ định bảng nào được dùng, gán property DataMember của DataGrid bằng tên của bảng. Trong ví dụ, chúng ta gán tên là Customers, là tên trùng với tên trong tham số thử hai trong phương thức Fill() của SqlDataAdapter. Đây là lý do tôi thích đặt tên bảng trong phương thức Fill(), và nó giúp đoạn mã dễ đọc hơn.

Cập nhật thay đổi

Sau khi thay đổi được thực hiện trên dữ liệu, bạn sẽ cần ghi lại vào database. Dòng mã sau cho thấy cách dùng phương thức Update của SqlDataAdapter để cập nhật các thay đổi vào database.

daCustomers.Update(dsCustomers, “Customers”);

Phương thức Update() trên được gọi trên thể hiện của SqlDataAdapter có tham số đầu tiên là chính đối tượng gọi phương thức. Tham số thử hai của phương thức Update() chỉ ra bảng nào trong DataSet sẽ được cập nhật. Bảng chứa một danh sách các dòng dữ liệu đã bị thay đổi và các property Insert, Update, Delete của SqlDataAdapter chứa các lệnh SQL dùng để thực hiện thay đối database.

Kết hợp tất cả lại

Tới bây giờ, bạn đã thấy các phần cần thiết để quản lý disconnected data. Cái bạn cần là xem tất cả công việc trong một ứng dụng. Listing 1 cho thấy cách đoạn mã từ các phần trước được dùng trong một chương trình hoàn chỉnh:

Listing 1: Implementing a Disconnected Data Management Strategy

using System;

using System.Data;

using System.Data.SqlClient;

using System.Drawing;

using System.Windows.Forms;

class DisconnectedDataform : Form

{

    private SqlConnection  conn;

    private SqlDataAdapter daCustomers;

    private DataSet  dsCustomers;

    private DataGrid dgCustomers;

    private const string tableName = "Customers";

    // initialize form with DataGrid and Button

    public DisconnectedDataform()

    {

        // fill dataset

        Initdata();

        // set up datagrid

        dgCustomers = new DataGrid();

        dgCustomers.Location = new Point(5, 5);

        dgCustomers.Size = new Size(

            this.ClientRectangle.Size.Width - 10,

            this.ClientRectangle.Height - 50);

        dgCustomers.DataSource = dsCustomers;

        dgCustomers.DataMember = tableName;

        // create update button

        Button btnUpdate = new Button();

        btnUpdate.Text = "Update";

        btnUpdate.Location = new Point(

            this.ClientRectangle.Width/2 - btnUpdate.Width/2,

            this.ClientRectangle.Height - (btnUpdate.Height + 10));

        btnUpdate.Click += new EventHandler(btnUpdateClicked);

        // make sure controls appear on form

        Controls.AddRange(new Control[] { dgCustomers, btnUpdate });

    }

    // set up ADO.NET objects

    public void Initdata()

    {

        // instantiate the connection

        conn = new SqlConnection(

            "Server=(local);DataBase=Northwind;Integrated Security=SSPI");

        // 1. instantiate a new DataSet

        dsCustomers = new DataSet();

        // 2. init SqlDataAdapter with select command and connection

        daCustomers = new SqlDataAdapter(

            "select CustomerID, CompanyName from Customers", conn);

        // 3. fill in insert, update, and delete commands

        SqlCommandBuilder cmdBldr = new SqlCommandBuilder(daCustomers);

        // 4. fill the dataset

        daCustomers.Fill(dsCustomers, tableName);

    }

    // Update button was clicked

    public void btnUpdateClicked(object sender, EventArgs e)

    {

        // write changes back to DataBase

        daCustomers.Update(dsCustomers, tableName);

    }

    // start the Windows form

    static void Main()

    {

        Application.Run(new DisconnectedDataForm());

    }

}

Phương thức Initdata() trong Listing 1 chứa các phương thức cần thiết để thiết lập SqlDataAdapter và DataSet. Lưu ý rằng các đối tượng dữ liệu khác nhau được định nghĩa tại cấp lớp vì thế chúng có thể được dùng trong nhiều phương thức. Property DataSource của DataGrid được gán trong constructor. Khi người dùng click nút Update, phương thức Update() trong sự kiện btnUpdateClickedsẽ được gọi, cập nhật thay đổi vào lại database.

Tổng kết

DataSet chứa nhiều bảng và có thể lưu trong bộ nhớ để tái sử dụng. SqlDataAdapter cho phép bạn đổ dữ liệu vào một DataSet và cập nhật thay đổi vào database. Bạn không cần lo lắng về việc mở và đóng SqlConnection bởi vì SqlDataAdapter tự động làm việc đó. Một SqlCommandBuilder tạo ra các câu lệnh insert, update, delete dựa trên câu select của SqlDataAdapter. Dùng phương thức Fill() của SqlDataAdapter để đổ dữ liệu vào DataSet. Gọi phương thức Update() của SqlDataAdapter để cập nhật thay đổi vào lại database.

Mời bạn trở lại cho bài kết tiếp trong series này. Lesson 06: Thêm Parameter vào SqlCommand.

Tags: