[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

  1. For simple domain logic.
  2. When design Domain Model pattern, choose either Active Record or  Data Mapper.
  3. If the application becomes more complex, Data Mapper is a better choice.
  4. Active Record is hard to do refactoring because of tightly coupling with database.
  5. 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)


<<:  Day2 参加职训(机器学习与资料分析工程师培训班),记录学习内容(6/30-8/20)

>>:  Day 26 (Js)

CSS微动画 - Transform不一定是位移的最佳选择

Q: 效能跟效果之间怎麽取舍? A: 如果效果不复杂,用一些渲染成本比较高的写法也无妨 新属性搭配...

DAY3-排序(二)

Merge Sort 原理:利用将两有序数组合并只需要线性时间的特性将数组分割,合并 思考&...

(World N0-1)! To Pass LookML-Developer Exam Guide

50% Discount On Google Updated LookML-Developer Ex...

Day18-持久不一定需要防腐剂 stateful redis建立

前面有特别提到,redis这类应用程序如果需要保持资料持久化,不会因为重启pod造成资料消失,就必须...

系统弱点扫描工具-Tenable Nessus(上)

系统弱点扫描也是我开始学习资安检测的第一个关卡 不论是网路管理人员、系统工程师到资安检测人员 了解弱...