Castle Active Record : Relationship

in

ต่อจากตอนที่แล้ว

สำหรับ Relation หรือความสัมพันธ์ที่ใช้ใน ActiveRecord จะมีหลักๆดังนี้

  • BelongsTo - ใช้ระบุความสัมพันธ์แบบ many-to-one หรือ one-to-one
  • HasMany - ใช้ระบุความสัมพันธ์แบบ one-to-many
  • HasAndBelongsToMany - ใช้ระบุความสัมพันธ์แบบ many-to-many

สมมติเราเพิ่มข้อมูลลงไปอีกตาราง คือ files เพื่อเอาไว้เก็บว่าแต่ละคนมีไฟล์อะไรบ้าง โดยมี Schema เป็นแบบนี้

schema

เมื่อเอามาแปลงเป็นคลาสก็จะได้หน้าตาเป็น

[ActiveRecord("files")]
public class UserFile : ActiveRecordBase<UserFile>
{
    [PrimaryKey(PrimaryKeyType.Native)]
    public virtual int ID { get; set; }

    [BelongsTo("user_id")]
    public virtual User Owner { get; set; }

    [Property("Filename")]
    public virtual string Filename { get; set; }

    [Property("type")]
    public virtual string ContentType { get; set; }

    [Property("data")]
    public virtual byte[] FileData { get; set; }

    [Property("size")]
    public virtual int FileSize { get; set; }
}

คราวนี้เปลี่ยนมา inherit จาก ActiveRecordBase แทนแล้ว โดยใน attribute ActiveRecord จะบอกว่าให้ map คลาสนี้เข้ากับตารางอะไร และสำหรับตรง property ก็จะบอกว่า map เข้ากับฟิลด์ไหนในตาราง

สำหรับ Property Owner จะเป็นการแมป Property นี้เข้ากับคลาส User โดยใช้ฟิลด์ user_id เพื่อบอกว่าจะแมปเข้ากับ object ไหนให้ดูจาก user_id เป็นหลัก

เซฟคลาสนี้ในไฟล์ชื่อ App_code/UserFile.cs

จากนั้นเราจะต้องไม่ลืมเพิ่มข้อมูลของคลาส UserFile ลงใน GlobalApplication.cs เพื่อให้ ActiveRecord รู้ว่าเราจะโหลดคลาสนี้เข้าไปด้วย

   public static void InitActiveRecord(IConfigurationSource source)
    {
    ActiveRecordStarter.Initialize(source,
        typeof(User),
        typeof(UserFile)

        )
        ;
    }

หลังจากนั้น เราก็จะต้องมาแก้ไขคลาส User เพื่อเพิ่ม Property Files เพื่อบอกว่ามีไฟล์อะไรเก็บไว้บ้าง
[ActiveRecord]
public class User : ActiveRecordValidationBase
{
[PrimaryKey(PrimaryKeyType.Native)]
public virtual int ID { get; set; }

    [Property]
    public virtual string Email { get; set; }

    [Property]
    public virtual string Password { get; set; }

    [HasMany(typeof(UserFile), "user_id", "files")]
    public virtual IList<UserFile> Files { get; set; }
}

ก่อนอื่นก็จะต้องสร้างหน้าฟอร์มสำหรับการอัพโหลดไฟล์ก่อน ตั้งชื่อว่า UploadForm.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UploadForm.aspx.cs" Inherits="UploadForm" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

    <asp:DropDownList ID="DropDownList1" runat="server" 
        DataSourceID="ObjectDataSource1" DataTextField="Email" DataValueField="ID">
    </asp:DropDownList>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" 
        SelectMethod="FindAll" TypeName="User"></asp:ObjectDataSource>

    </div>
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <br />
    <asp:Button ID="Button1" runat="server" onclick="Button1_Click" Text="Button" />
    <asp:Label ID="Label1" runat="server"></asp:Label>
    </form>
</body>
</html>

โดยมีเนื้อหาของ Code Behind เป็น

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Xml.Linq;

public partial class UploadForm : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {    }
    protected void Button1_Click(object sender, EventArgs e)
    {
    UserFile uf = new UserFile();
    uf.Filename = FileUpload1.FileName;
    uf.FileSize = FileUpload1.FileBytes.Length;
    uf.ContentType = FileUpload1.PostedFile.ContentType;
    uf.FileData = FileUpload1.FileBytes;

    uf.Owner = global::User.Find(Convert.ToInt32(DropDownList1.SelectedValue));
    uf.Save();

    Label1.Text = "Upload Completed";
    }
}

จากนั้นลองอัพโหลดไฟล์เข้าไปซัก 2 - 3 ไฟล์

upload

ทีนี้เราลองกลับมาแก้ไขหน้า default ให้แสดงผลรายการของไฟล์ด้วย

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataSourceID="ObjectDataSource1">
    <Columns>
        <asp:BoundField DataField="ID" HeaderText="ID" SortExpression="ID" />
        <asp:BoundField DataField="Email" HeaderText="Email" SortExpression="Email" />
        <asp:BoundField DataField="Password" HeaderText="Password" SortExpression="Password" />
        <asp:TemplateField HeaderText="File">
        <ItemTemplate>
            <asp:Repeater ID="Repeater1" runat="server" DataSource='<%# Eval("Files") %>'>
            <HeaderTemplate>
                <ul>
            </HeaderTemplate>
            <ItemTemplate>
                <li>
                <%#Eval("Filename") %>
                (<%#Eval("FileSize") %>)</li></ItemTemplate>
            <FooterTemplate>
                </ul></FooterTemplate>
            </asp:Repeater>
        </ItemTemplate>
        </asp:TemplateField>
    </Columns>
    </asp:GridView>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="FindAll"
    TypeName="User"></asp:ObjectDataSource>
    </form>
</body>
</html>

ที่ทำคือเพิ่ม template field เข้าไปหนึ่งคอลัมน์ใน GridView จากนั้นก็ Bind ตัว property Files เข้าไปกับ Repeater แล้วให้ Repeater แสดงชื่อไฟล์พร้อมกับขนาดออกมา

data
m2m

ทีนี้สมมติว่าเราใส่ tag ให้กับไฟล์ด้วยล่ะ จะทำยังไง

สมมติให้มีอีกตารางชื่อว่า tags

tag

และตารางที่ทำหน้าที่เชื่อมระหว่าง tags และ files ให้ชื่อว่า file_tag

file_tag

ทีนี้สำหรับคลาส Tag ก็ทำคล้ายๆคลาส UserFile ก็คือดังข้างล่าง

using System;
using Castle.ActiveRecord;
using System.Collections.Generic;


[ActiveRecord("Tags")]
public class Tag : ActiveRecordBase<Tag>
{
    [PrimaryKey(PrimaryKeyType.Native)]
    public virtual int ID { get; set; }

    [Property("tag")]
    public virtual string TagName { get; set; }

    [HasAndBelongsToMany(typeof(UserFile), Table="file_tag", ColumnKey="tag_id", ColumnRef="file_id", Inverse=true )]
    public virtual IList<UserFile> Files { get; set; }

    public static Tag FindByTag(string tag)
    {
    Tag t = Tag.FindFirst(Expression.Like("TagName", tag));
    return t;

    }
}

ตรงนี้เพิ่มเมธอด FindByTag เข้ามาโดยจะดูว่ามี Tag ดังกล่าวอยู่ในระบบอยู่แล้วหรือไม่

ทีนี้ลองกลับไปแก้ คลาส UserFile โดยใส่ Property Tags เข้าไปดูบ้าง

[ActiveRecord("files")]
public class UserFile : ActiveRecordBase<UserFile>
{
    [PrimaryKey(PrimaryKeyType.Native)]
    public virtual int ID { get; set; }

    [BelongsTo("user_id")]
    public virtual User Owner { get; set; }

    [Property("Filename")]
    public virtual string Filename { get; set; }

    [Property("type")]
    public virtual string ContentType { get; set; }

    [Property("data")]

    public virtual byte[] FileData { get; set; }

    [Property("size")]
    public virtual int FileSize { get; set; }

            [HasAndBelongsToMany(typeof(Tag), RelationType.Bag, Table="file_tag", ColumnKey="file_id", ColumnRef="tag_id")]
    public virtual IList<Tag> Tags { get; set; }

}

จัดการแก้หน้า UploadForm ให้รองรับเรื่อง Tag ด้วยการเพิ่ม textbox เข้าไปหนึ่งอัน

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UploadForm.aspx.cs" Inherits="UploadForm" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:DropDownList ID="DropDownList1" runat="server" DataSourceID="ObjectDataSource1"
        DataTextField="Email" DataValueField="ID">
    </asp:DropDownList>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="FindAll"
        TypeName="User"></asp:ObjectDataSource>
    </div>
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <br />
    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
    <br />
    <asp:Button ID="Button1" runat="server" OnClick="Button1_Click" Text="Button" />
    <asp:Label ID="Label1" runat="server"></asp:Label>
    </form>
</body>
</html>

จากนั้นก็ไปแกตอนกดปุ่ม

protected void Button1_Click(object sender, EventArgs e)
    {
    UserFile uf = new UserFile();
    uf.Filename = FileUpload1.FileName;
    uf.FileSize = FileUpload1.FileBytes.Length;
    uf.ContentType = FileUpload1.PostedFile.ContentType;
    uf.FileData = FileUpload1.FileBytes;
    uf.Tags = new List<Tag>();
    foreach (string tag in TextBox1.Text.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries ))
    {
        string trimmedTag = tag.Trim();
        if (!string.IsNullOrEmpty(trimmedTag))
        {
        Tag t = Tag.FindByTag(trimmedTag);

        if (t == null)
        {
            t = new Tag();
            t.TagName = trimmedTag;
            t.Save();
        }

        uf.Tags.Add(t);
        }
    }
    uf.Owner = global::User.Find(Convert.ToInt32(DropDownList1.SelectedValue));
    uf.Save();

    Label1.Text = "Upload Completed";
    }

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

วิธีทดสอบง่ายๆก็เลยลองสร้างหน้าที่ใช้แสดงรายการไฟล์ต่างๆของ tag ขึ้นมาดู

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default2.aspx.cs" Inherits="Default2" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:DropDownList ID="DropDownList1" runat="server" AutoPostBack="True" DataSourceID="ObjectDataSource1"
        DataTextField="TagName" DataValueField="ID" OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged">
    </asp:DropDownList>
    <asp:GridView ID="GridView1" runat="server">
    </asp:GridView>
    </div>
    <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="FindAll"
    TypeName="Tag"></asp:ObjectDataSource>
    </form>
</body>
</html>

โดยมี Code Behind เป็น

using System;

public partial class Default2 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {    }
    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
    GridView1.DataSource = Tag.Find(Convert.ToInt32(DropDownList1.SelectedValue)).Files;
    GridView1.DataBind();
    }
}

เมื่อทดลองรันดู ก็ได้ผลน่าพอใจสำหรับการใช้งาน Active Record แล้วนะ

out

สำหรับตอนนี้ก็ขอจบลงเพียงเท่านี้ ในตอนหน้ากำลังคิดอยู่ว่าจะเป็นเรื่อง transaction, validation หรือจะเป็นเรื่อง lazy loading ดี

ไว้เจอกันเมื่อหาเวลาได้

น่าสนใจมาก เลยคะ ตกลงแล้ว ORM

น่าสนใจมาก เลยคะ

ตกลงแล้ว ORM ตัวไหน น่าใช้ที่สุดคะ สำหรับ .NET

Post new comment

The content of this field is kept private and will not be shown publicly.