Achieving POCOs in Linq to SQL

Bài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết Achieving POCOs in Linq to SQL. Code sample trong bài tham khảo 99,99% từ bài viết gốc của tác giả Sidar Ok.


- LinQ2SQL là một công cụ khá hữu dụng trong các ORM tools hiện nay. Tuy nhiên các lớp Entity sinh ra thường khiến chúng ta không hài lòng vì chúng không phải những lớp C# thuần túy. Đối với một số người, các lớp Entity trong hệ thống cần phải là những POCO, có lẽ để không phải phụ thuộc vào LinQ vì chúng ta biết rằng những lớp này bên trong nó có sử dụng những lớp và interface khác của LinQ. Giảm phụ thuộc là một điều tốt. Tuy từ lúc làm lập trình viên tới giờ tui chưa bao giờ phải rơi vào trường hợp phải thay đổi công nghệ như tình huống chúng ta đang lo xa, nhưng ai mà biết trước được điều gì sẽ xảy ra cho project...:bbpktay:

- Một ngày đẹp trời nào đó, ta quyết định sử dụng LinQ2SQL để làm DataAccess nhưng lại muốn làm sao để dễ thay đổi khi có một kĩ thuật khác hay và có nhiều ưu điểm hơn LinQ2SQL ra đời. Cho nên lựa chọn tốt nhất là giữ cho các lớp Entity thật đơn giản và không phụ thuộc chút gì đến LinQ2SQL. Để thực hiện được điều đó, chúng ta nên biết rằng các lớp Entity được sinh tự động sẽ bị phụ thuộc vào lớp EntitySet, EntityRef, INotifyPropertyChangingINotifyPropertyChanged. Vậy không phải chỉ cần bỏ những nơi sử dụng các lớp ấy sao? Các lớp và interface này được tạo ra là có lý do của nó, nếu bỏ đi thì chắc chắn chúng ta sẽ phải hi sinh một số tính năng mà LinQ2SQL hỗ trợ. Những tính năng đó là gì?:bbpnen:

+ Khi bạn thêm một Entity mới vào một danh dách, EntitySet sẽ quản lý entity này cho bạn đồng thời nó cũng cập nhật mối quan hệ giữa entity này với parent của nó. Tương tự đối với EntityRef

+ INotifyPropertyChangingINotifyPropertyChanged giúp chúng ta đăng kí những sự kiện cần thiết khi một property của Entity thay đổi và nó cũng giúp hỗ trợ chức năng Lazy Load của LinQ2SQL. Vì vậy khi quyết định bỏ không sử dụng 2 interface trên, chúng ta sẽ mất đi tính năng Lazy Load của LinQ2SQL.

- Trong một số trường hợp Lazy Load rất hay nhưng đôi khi nó cũng gây khá nhiều phiền phức. Tôi chỉ sử dụng LinQ2SQL để làm các công việc cập nhật, update hoặc thêm một object đơn giản, những trường hợp phức tạp như xuất report, tôi vẫn sử dụng Stored Procedure và rất may là LinQ2SQL có hỗ trợ Stored Procedure.:bbpxtay:

- Trong bài này, chúng ta sẽ tìm cách né EntitySetEntityRef. Còn Lazy Loading sẽ nói trong bài khác. Khi sử dụng LinQ2SQL, có 2 cách để map các property của Entity với các column của table trong database. Chúng ta sẽ sử dụng XML mapping thay vì sử dụng attribute để giữ cho các lớp Entity trong sạch :bbpcuoi3:. Ví dụ được sử dụng trong bài này là các model QuestionsAnswers. Một Questions có thể có nhiều Answers tương ứng với nó:

Class Diagram

Hình 1: Class Question và Answer ví dụ trong bài viết

- Và sau đây là code POCO của lớp QuestionsAnswer. Chúng ta có thể sử dụng tool sqlmetal.exe để sinh ra các lớp này và file xml mapping. Tất nhiên chúng ta phải copy và paste đồng thời sửa đổi những phần cần thiết từ các file sinh ra để được những lớp POCO mong muốn.

public class Answer
{

public Answer()
{ }

private int _AnswerId;
public int AnswerId
{

get { return _AnswerId; }
set { _AnswerId = value; }

}

private int _QuestionId;
public int QuestionId
{

get { return _QuestionId; }
set { _QuestionId = value; }

}

private string _AnswerText;
public string AnswerText
{

get { return _AnswerText; }
set { _AnswerText = value; }

}

private bool _IsMarkedAsCorrect;
public bool IsMarkedAsCorrect
{

get { return _IsMarkedAsCorrect; }
set { _IsMarkedAsCorrect = value; }

}

private int _Vote;
public int Vote
{

get { return _Vote; }
set { _Vote = value; }

}

}

Code 1: Code của lớp POCO Answer
Các bạn đang xem bài viết về Achieving POCOs in Linq to SQL từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


- Như các bạn thấy code của lớp Answer rất sạch sẽ, không có Attribute, không EntityRef, EntitySet gì cả. Tương tự đối với lớp Questions, vì một Questions có thể có nhiều quan hệ với các Answer nên ta sẽ sử dụng List như sau::bbpraroi:

public class Question
{

public Question()
{ }

private int _QuestionId;
public int QuestionId
{

get { return _QuestionId; }
set { _QuestionId = value; }

}

private string _QuestionText;
public string QuestionText
{

get { return _QuestionText; }
set { _QuestionText = value; }

}

private List<Answer> _Answers;
public List<Answer> Answers
{

get { return _Answers; }
set { _Answers = value; }

}

}

Code 2: Code của lớp POCO Question

- Chúng ta dung tool SQLMetal.exe để sinh ra file mapping cần thiết. File xml khi sinh ra sẽ map các column vào các property của lớp Entity như trong hình. Chúng ta sẽ phải sửa một số chỗ như Type Name cho đúng namespace của các lớp Entity:

<?xml version="1.0" encoding="utf-8"?>
<Database Name="POCOLINQ" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">
<Table Name="dbo.Answer" Member="Answer">
<Type Name="POCODemo.Entity.Answer">
<Column Name="AnswerId" Member="AnswerId" Storage="_AnswerId" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Column Name="QuestionId" Member="QuestionId" Storage="_QuestionId" DbType="Int NOT NULL" />
<Column Name="AnswerText" Member="AnswerText" Storage="_AnswerText" DbType="Text NOT NULL" CanBeNull="false" UpdateCheck="Never" />
<Column Name="IsMarkedAsCorrect" Member="IsMarkedAsCorrect" Storage="_IsMarkedAsCorrect" DbType="Bit NOT NULL" />
<Column Name="Vote" Member="Vote" Storage="_Vote" DbType="Int NOT NULL" />
<Association Name="FK_Answer_Question" Member="Question" Storage="_Question" ThisKey="QuestionId" OtherKey="QuestionId" IsForeignKey="true" />

</Type>

</Table>
<Table Name="dbo.Question" Member="Question">
<Type Name="POCODemo.Entity.Question">
<Column Name="QuestionId" Member="QuestionId" Storage="_QuestionId" DbType="Int NOT NULL IDENTITY" IsPrimaryKey="true" IsDbGenerated="true" AutoSync="OnInsert" />
<Column Name="QuestionText" Member="QuestionText" Storage="_QuestionText" DbType="NVarChar(300) NOT NULL" CanBeNull="false" />
<Association Name="FK_Answer_Question" Member="Answers" Storage="_Answers" ThisKey="QuestionId" OtherKey="QuestionId" DeleteRule="NO ACTION" />

</Type>

</Table>

</Database>

Code 3: File xml mapping(đã được sửa đổi)

- SQLMetal.exe sẽ sinh ra một lớp DataContext cùng với nội dung các lớp Entity. Chúng ta đã copy code sang các lớp Entity, tất nhiên cũng phải tạo một lớp DataContext riêng để sử dụng.


C:\>sqlmetal /server:localhost\SQLEXPRESS /database:POCOLINQ /map:Mapping.xml /code:QuestionDataContext.cs
Microsoft (R) Database Mapping Generator 2008 version 1.00.21022
for Microsoft (R) .NET Framework version 3.5
Copyright (C) Microsoft Corporation. All rights reserved.

C:\>


Hình 2: Câu lệnh sinh code sqlmetal


public class QuestionDataContext : DataContext
{

static XmlMappingSource source = XmlMappingSource.FromXml(File.ReadAllText("Mapping.xml"));
static string connStr = "Data Source=localhost\\SQLEXPRESS;Initial Catalog=POCOLINQ;Integrated Security=True";

public QuestionDataContext()
: base(connStr, source)
{
}

public Table<Answer> Answers
{

get
{

return this.GetTable<Answer>();

}

}

public Table<Question> Questions
{

get
{

return this.GetTable<Question>();

}

}

}

Code 4: Lớp QuestionDataContext

- Bây giờ chúng ta sẽ thử viết một unit test xem code của chúng ta có truy xuất được database và đọc dữ liệu được không.
:bbpsdieu2:

[TestMethod]
public void GetQuestionTest()
{

QuestionRepository qRepository = new QuestionRepository();
int id = 2;
Question actual;
actual = qRepository.GetQuestion(id);
Assert.IsNotNull(actual);
Assert.IsTrue(actual.Answers.Count > 0);

}

Code 5: Unit Test get question by id

- Để code build thành công, chúng ta sẽ implement lớp QuestionRepository. Do đã lược bỏ các tính năng của LinQ2SQL, trong trường hợp này chúng ta phải làm điều đó bằng tay để load tất cả các Answer nếu có ứng với Questions trong database:

public class QuestionRepository
{

public Question GetQuestion(int id)
{

using (QuestionDataContext context = new QuestionDataContext())
{

DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Question>(q => q.Answers);
context.LoadOptions = options;
return context.Questions.Single<Question>(q => q.QuestionId == id);

}

}

}

Code 6: Lớp QuestionRepository
Các bạn đang xem bài viết về Achieving POCOs in Linq to SQL từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


- Khi run test, các bạn sẽ thấy test method sẽ pass, như vậy là đã đọc được từ database. Để chắc ăn chúng ta sẽ test các câu lệnh Insert:
:bbpraroi:

[TestMethod]
public void InsertQuestionTest()
{

QuestionRepository qRepository = new QuestionRepository();
Question question = new Question()
{

QuestionText = "Temp Question",
Answers = new List<Answer>()
{

new Answer()
{

AnswerText = "Temp Answer 1",
IsMarkedAsCorrect = true,
Vote = 10,

},

new Answer()
{

AnswerText = "Temp Answer 2",
IsMarkedAsCorrect = false,
Vote = 10,

},

new Answer()
{

AnswerText = "Temp Answer 3",
IsMarkedAsCorrect = true,
Vote = 10,

},

}

};

using (TransactionScope scope = new TransactionScope())
{

qRepository.InsertQuestion(question);
Assert.IsTrue(question.QuestionId > 0);
Assert.IsTrue(question.Answers[0].AnswerId > 0);
Assert.IsTrue(question.Answers[1].AnswerId > 0);
Assert.IsTrue(question.Answers[2].AnswerId > 0);

}

}

Code 7: Unit test câu lệnh Insert

- Để code build được, chúng ta viết hàm InsertQuestion cho QuestionRepository
:bbpraroi:

public void InsertQuestion(Question q)
{

using (QuestionDataContext context = new QuestionDataContext())
{

context.Questions.InsertOnSubmit(q);
context.SubmitChanges();

}

}

Code 8: Hàm Insert Question

- Khi run test này, chúng ta sẽ thấy lỗi sau đây:


TestCase 'POCODemo.Test.QuestionMappingTest.InsertQuestionTest'
failed: System.Data.SqlClient.SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Answer_Question".
The conflict occurred in database "POCOLINQ", table "dbo.Question", column 'QuestionId'.
The statement has been terminated.


Hình 3: Test insert failed

-Chúng ta biết rằng để save được các Entity Answer thì cần phải biết ID của Questions tương ứng. Nhưng làm thể nào để biết được ID của Questions. Vấn để ở chỗ back reference. Chúng ta cần phải tạo một reference đến parent Questions của mỗi Answer bởi vì chúng ta đã bỏ đi EntitySet nên giờ phải tự làm theo cách của chúng ta. Ta modify lại lớp QuestionsAnswer như sau:

// Class Answer
private Question _Question;
public Question Question
{

get { return _Question; }
set
{
_Question = value;
this._QuestionId = value.QuestionId;

}

}


// Class Question
private List<Answer> _Answers;
public List<Answer> Answers
{

get { return _Answers; }
set
{

_Answers = value;
foreach (var answer in _Answers)
{

answer.QuestionId = this.QuestionId;
answer.Question = this;

}

}

}

Code 9: Modify lớp Question và Answer
Các bạn đang xem bài viết về Achieving POCOs in Linq to SQL từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)


- Như vậy là test của chúng ta đã pass.

Kết luận: Mong muốn để đạt được tính decoupling giữa các domain entities và các kĩ thuật trong chương trình là một trong những vấn đề quan trọng mà ta hay gặp. Đối với LinQ2SQL, chúng ta phải quên đi các lớp EntityRef, EntitySet, InotifyPropertyChanged, INotifyPropertyChanging . Các công nghệ của Microsoft nói chung và LinQ nói riêng được thiết kế để rút ngắn thời gian lập trình. Quả thật nếu không quan tâm đến POCO thì có thể nói khi thiết kế xong Database thì với một câu lệnh sqlmetal ta đã có được DataAccessLayer. Theo tôi nên cân nhắc giữa các lựa chọn và tùy vào quy mô Project để có cách sử dụng thích hợp.
(Còn tiếp)
:bbpcheer: Happy New Year

Code sample: http://nthoaiblog.googlepages.com/POCOLINQ2SQL.zip


Posted in Labels: , , , |

3 comments:

  1. hien pham Says:

    Cảm ơn bác Nguyễn Thoại nhiều nhe.Blog của bác giúp mình nhiều lắm nhất là PaggingLinQ2SQLnày...Thank's

  2. Anonymous Says:

    thanks, bài viết hữu ích
    ----------
    TTA

  3. niku Says:

    Ý nghĩa của POCO : http://en.wikipedia.org/wiki/Plain_Old_CLR_Object

rss
 

About Me

Place I've live
Near Bossley Park, Sydney, NSW, Australia
Place I've work
  • Freelancer (from 06/2010 to present)
  • Harvey Nash (from 05/2008 to 06/2010)
  • DataDesign Vietnam (10/2005 to 04/2008)
Place I've studied
  • University of Natural Science (Bachelor of Science HoChiMinh City Vietnam From 2001 to 2005)
  • Le Hong Phong High School (HoChiMinh City Vietnam From 1997 to 2000)