[PoEAA] Data Source Architectural Pattern - Active Record

本篇同步发布于个人Blog: [PoEAA] Data Source Architectural Pattern - Active Record

1. What is Active Record

According to [PoEAA], this definition is "An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data."

Figure 1. Active Record (From PoEAA Page)

1.1 How it works

It is a Domain Model. Every Active Record's class is mapped to a record of the database and loads the data source to process with domain logic.
An approximate equality: Active Record ≈ Row Data Gateway + Domain Logic

1.2 When to use it

For simple domain logic.When design Domain Model pattern, choose either Active Record or  Data Mapper.If the application becomes more complex, Data Mapper is a better choice.Active Record is hard to do refactoring because of tightly coupling with database.When use Transaction Script, Row Data Gateway gradually evolves into Active Record.

2. Pattern Practice: The Person Management

This problem is introduced in the previous article [PoEAA] Data Source Architectural Pattern - Table Data Gateway. This article uses Active Record to build the data source architectural layer.

2.1 Implementation by C# & SQLite

This pattern is implemented by C# based on the content of Chapter 10 Data Source Architectural Patterns - Active Record of PoEAA. The database is SQLite.

By Martin's implementation, it doesn't contain a Delete function in a active record. So this article also has no Delete function.

2.1.1 Person class

This Person class creates Insert/Update/Find/Load basic functions to manipulate person table. One instance function GetExemption() is a business logic.

    class Person : BaseActiveRecord    {    public string LastName { get; set; }    public string FirstName { get; set; }    public int NumberOfDependents { get; set; }    private const string FindStatementString = @"    SELECT id, lastname, firstname, numberOfDependents    FROM person    WHERE id = $id    ";        private const string UpdateStatementString =    @"UPDATE person     SET lastname = $lastname, firstname = $firstname, numberOfDependents = $numberOfDependents    where id = $id";        private const string InsertStatementString =    @"INSERT INTO person     VALUES ($id, $lastname, $firstname, $numberOfDependents)";        public Person(int id, string lastName, string firstName, int numberOfDependents)    {    Id = id;    LastName = lastName;    FirstName = firstName;    NumberOfDependents = numberOfDependents;    }        public static Person Find(int id)    {    Person result = Registry.GetPerson(id);    if (result != null)    {    return result;    }        try    {    using var conn = DbManager.CreateConnection();    conn.Open();    using IDbCommand comm = new SQLiteCommand(FindStatementString, conn);    comm.Parameters.Add(new SQLiteParameter("$id", id));    using IDataReader reader = comm.ExecuteReader();    reader.Read();    result = Load(reader);    return result;    }    catch (Exception ex)    {    throw new ApplicationException(ex.Message);    }    }        public static Person Load(IDataReader reader)    {    object[] resultSet = new object[reader.FieldCount];    reader.GetValues(resultSet);        int id = (int)resultSet[0];    Person result = Registry.GetPerson(id);    if (result != null)    {    return result;    }        string lastName = resultSet[1].ToString();    string firstName = resultSet[2].ToString();    int numberOfDependents = (int)resultSet[3];    result = new Person(id, lastName, firstName, numberOfDependents);    Registry.AddPerson(result);    return result;    }        public void Update()    {    try    {    using var conn = DbManager.CreateConnection();    conn.Open();    using IDbCommand comm = new SQLiteCommand(UpdateStatementString, conn);    comm.Parameters.Add(new SQLiteParameter("$lastname", LastName));    comm.Parameters.Add(new SQLiteParameter("$firstname", FirstName));    comm.Parameters.Add(new SQLiteParameter("$numberOfDependents", NumberOfDependents));    comm.Parameters.Add(new SQLiteParameter("$id", Id));    comm.ExecuteNonQuery();    }    catch (Exception ex)    {    throw new ApplicationException(ex.Message);    }    }        public int Insert()    {    try    {    using var conn = DbManager.CreateConnection();    conn.Open();    using IDbCommand comm = new SQLiteCommand(InsertStatementString, conn);    Id = FindNextDatabaseId();    comm.Parameters.Add(new SQLiteParameter("$id", Id));    comm.Parameters.Add(new SQLiteParameter("$lastname", LastName));    comm.Parameters.Add(new SQLiteParameter("$firstname", FirstName));    comm.Parameters.Add(new SQLiteParameter("$numberOfDependents", NumberOfDependents));    comm.ExecuteNonQuery();    Registry.AddPerson(this);        return Id;    }    catch (Exception ex)    {    throw new ApplicationException(ex.Message);    }    }        public Money GetExemption()    {    Money baseExemption = Money.Dollars(1500d);    Money dependentExemption = Money.Dollars(750d);    return baseExemption.Add(dependentExemption.Multiply((double) NumberOfDependents));    }        private int FindNextDatabaseId()    {    string sql = "SELECT max(id) as curId from person";    using var conn = DbManager.CreateConnection();    conn.Open();    using IDbCommand comm = new SQLiteCommand(sql, conn);    using IDataReader reader = comm.ExecuteReader();    bool hasResult = reader.Read();    if (hasResult)    {    return ((int)((long)reader["curId"] + 1));    }    else    {    return 1;    }    }    }        internal class BaseActiveRecord    {    public int Id { get; protected set; }    }

2.1.2 Registry

The Registry has been used in [PoEAA] Data Source Architectural Pattern - Row Data Gateway. This article uses it to register Person instances.

    internal class Registry    {    private static readonly Registry Instance = new Registry();    private readonly Dictionary<int, Person> _personsMap = new Dictionary<int, Person>();        private Registry()    {        }        public static void AddPerson(Person person)    {    Instance._personsMap.Add(person.Id, person);    }        public static Person GetPerson(int id)    {    if (Instance._personsMap.ContainsKey(id))    {    return Instance._personsMap[id];    }        return null;    }    }

2.2 Demo

Create a console program and create 3 Persons in SQLite as the following code:

    class Program    {    private const string FindAllPersonsStatementString = @"    SELECT id, lastname, firstname, numberOfDependents    FROM person    ";        static void Main(string[] args)    {    InitializeData();        Console.WriteLine("Get persons");    var people = FindPersons();    PrintPerson(people);        Console.WriteLine("Insert a new person");    new Person(0, "Rose", "Jackson", 60).Insert();    people = FindPersons();    PrintPerson(people);        Console.WriteLine("Update a person's first name");    var firstPerson = Person.Find(1);    firstPerson.FirstName = "Jack";    firstPerson.Update();        Console.WriteLine("Update a person's number of dependents");    var secondPerson = Person.Find(2);    secondPerson.NumberOfDependents = 0;    secondPerson.Update();        Console.WriteLine("Get persons again");    people = FindPersons();    PrintPerson(people);    }        private static List<Person> FindPersons()    {    List<Person> result = new List<Person>();    try    {    using var conn = DbManager.CreateConnection();    conn.Open();    using IDbCommand comm = new SQLiteCommand(FindAllPersonsStatementString, conn);    using IDataReader reader = comm.ExecuteReader();    while (reader.Read())    {    result.Add(Person.Load(reader));    }        return result;    }    catch (Exception ex)    {    throw new ApplicationException(ex.Message);    }    }        private static void PrintPerson(IEnumerable<Person> people)    {    foreach (var person in people)    {    Console.WriteLine($"ID: {person.Id}, " +      $"last name: {person.LastName}, " +      $"first name: {person.FirstName}, " +      $"number of dependents: {person.NumberOfDependents}, " +      $"exemption: {person.GetExemption().Amount}");    }    }        private static void InitializeData()    {    using (var connection = DbManager.CreateConnection())    {    connection.Open();        using (var command = connection.CreateCommand())    {    command.CommandText =    @"    DROP TABLE IF EXISTS person;    ";    command.ExecuteNonQuery();            command.CommandText =    @"    CREATE TABLE person (Id int primary key, lastname TEXT, firstname TEXT, numberOfDependents int);    ";    command.ExecuteNonQuery();        command.CommandText =    @"           INSERT INTO person    VALUES (1, 'Sean', 'Reid', 5);        INSERT INTO person    VALUES (2, 'Madeleine', 'Lyman', 13);        INSERT INTO person    VALUES (3, 'Oliver', 'Wright', 66);    ";    command.ExecuteNonQuery();    }        }    }    }

The console shows:

3. Conclusions

"Active Record" is a advanced version of Row Data Gateway. The above sample code is uploaded to this Github Repository.

For next article I will write Active Record pattern according to Chapter 10 Data Source Architectural Pattern - Data Mapper of PoEAA.

4. References

Patterns of Enterprise Application Architecture Book(Amazon)


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章