Xây dựng Microservices bằng ASP.NET Core – Thao tác với database bằng Marten
Danh sách các bài viết:
- Phần 1: Lập kế hoạch.
- Phần 2: Định hình kiến trúc Microservices với CQRS và MediatR.
- Phần 3: Khám phá dịch vụ với Eureka.
- Phần 4: Xây dựng API Gateway với Ocelot.
- Phần 5: Thao tác với database PostgreSQL bằng Marten (bài viết này).
- Phần 6: Giao tiếp máy chủ thời gian thực với SignalR và RabbitMQ.
- Phần 7: Transaction outbox với RabbitMQ.
Trong bài viết này, chúng ta sẽ quay lại một chút và nói về truy cập dữ liệu và cách lưu trữ dữ liệu một cách hiệu quả.
Mã nguồn cho giải pháp hoàn chỉnh có thể được tìm thấy trên GitHub của chúng tôi.
Kiên trì là một vấn đề đã được giải quyết, phải không?
Khi các phiên bản đầu tiên của .NET Framework ra mắt vào khoảng năm 2002, chúng ta có hai API chính để truy cập dữ liệu: DataSet và DataReader.
DataSet biểu diễn bộ nhớ của các bảng trong cơ sở dữ liệu của bạn, DataReader cho phép bạn đọc dữ liệu của mình nhanh chóng, nhưng bạn phải đẩy nó vào các đối tượng của mình theo cách thủ công.
Nhiều nhà phát triển đã khám phá ra sức mạnh của reflection và hầu hết mọi người đều phát triển ORM của riêng họ. Ngày trước, nhóm của tôi đã đánh giá một số framework như vậy, nhưng không có framework nào trong số này dường như là giải pháp thích hợp cho chúng tôi, vì chúng tôi đang xây dựng các ứng dụng phức tạp cho ngành bảo hiểm.
Vì vậy, chúng tôi quyết định sử dụng DataReader và SQL được viết code thủ công cho mỗi thao tác insert, update và search. Vài năm sau, chúng tôi xây dựng một thứ mà ngày nay được gọi là Micro ORM. Ngay cả với công cụ tự trồng đơn giản này của chúng tôi đã có thể loại bỏ khoảng 70% mã truy cập dữ liệu.
Sau đó là thời đại của NHibernate. Là một nhà phát triển có kinh nghiệm về Java, tôi rất ghen tị khi các đồng nghiệp Java của tôi có một thư viện tuyệt vời như vậy và khi các phiên bản đầu tiên của NHibernate có sẵn, tôi rất háo hức dùng thử.
Tôi nghĩ đó là phiên bản 2.0 khi chúng tôi bắt đầu sử dụng NHibernate trong môi trường sản xuất. Trong nhiều năm, NHibernate là sự lựa chọn hàng đầu của chúng tôi và đã giúp chúng tôi trong nhiều dự án. Đó là thư viện linh hoạt và giàu tính năng đáng kinh ngạc.
Nhưng Microsoft đã quyết định triển khai giải pháp độc quyền của riêng họ – Entity Framework. Vì nó được quảng bá rầm rộ, nhiều nhà phát triển .NET quyết định chuyển sang EF, sự phổ biến của NHibernate và cộng đồng bắt đầu thu hẹp lại.
Sau đó, Microsoft giới thiệu .NET Core và một lần nữa mọi thứ đã thay đổi. Họ đã không chuyển đổi Entity Framework hiện có mà thay vào đó họ quyết định phát triển một phiên bản mới của nó từ đầu là Entity Framework Core.
Do đó, các phiên bản đầu tiên của .NET Core và EF Core không thực sự có giải pháp cấp doanh nghiệp để truy cập dữ liệu. Chúng vẫn còn thiếu nhiều tính năng mà bạn mong đợi từ ORM trưởng thành.
NHibernate cuối cùng đã có mặt trên .NET Core nhưng tôi không nghĩ nó sẽ trở nên phổ biến vì cộng đồng xung quanh nó nhỏ hơn nhiều. NHibernate so với các ORM ngày nay dường như rất khó sử dụng, ví dụ như nó buộc bạn phải khai báo tất cả các thuộc tính của mình là virtua để có thể được hỗ trợ bởi ORM.
Sự xuất hiện của .NET Core và sự phổ biến ngày càng tăng của microservices đã thay đổi hoàn toàn cảnh quan kiến trúc .NET. Bây giờ bạn có thể phát triển và triển khai trên Linux. Việc sử dụng các cơ sở dữ liệu khác ngoài MS SQL Server ngày càng trở nên phổ biến hơn trong giới lập trình viên .NET.
Microservices cũng làm tăng tính phổ biến về lưu trữ dữ liệu trên các kho dữ liệu khác nhau. Các nhà phát triển nhận ra rằng họ có thể sử dụng các kho dữ liệu khác nhau cho các loại dịch vụ khác nhau. Có cơ sở dữ liệu tài liệu, cơ sở dữ liệu đồ thị, kho sự kiện và các loại giải pháp liên quan đến cơ sở dữ liệu.
Như bạn có thể thấy, có rất nhiều tùy chọn để lựa chọn và một trong những tùy chọn mà tôi muốn nói đến trong bài đăng này là sử dụng cơ sở dữ liệu quan hệ làm cơ sở dữ liệu tài liệu, để tận dụng tối đa cả hai thế giới. Bạn có thể đạt được điều này với sự giúp đỡ nhỏ của Marten.
Marten là gì?
Marten là một thư viện cho phép các nhà phát triển .NET sử dụng Postgresql làm cơ sở dữ liệu tài liệu và như một kho lưu trữ sự kiện (event store). Nó được bắt đầu bởi Jeremy Miller để thay thế cho cơ sở dữ liệu RavenDB vào khoảng tháng 10 năm 2015, nhưng nó còn hơn thế nữa.
Nếu bạn đã từng làm việc với cơ sở dữ liệu tài liệu như MongoDB hoặc RavenDB, bạn biết rằng nó mang lại cho bạn trải nghiệm tuyệt vời dành cho nhà phát triển, đặc biệt là tính dễ sử dụng và tốc độ phát triển, nhưng có một số vấn đề nhất định liên quan đến hiệu suất và tính nhất quán của dữ liệu.
Với Marten, bạn có thể dễ dàng sử dụng cơ sở dữ liệu quan hệ làm tài liệu, với sự tuân thủ ACID đầy đủ và hỗ trợ LINQ rộng rãi.
Theo quan điểm của tôi, có một trường hợp sử dụng nhất định có vẻ lý tưởng cho kiểu tiếp cận này. Nếu bạn đang thực hành thiết kế theo hướng miền (domain-driven design – DDD) và phân vùng mô hình miền của bạn thành các tập hợp nhỏ, bạn có thể coi các tập hợp của mình là tài liệu.
Nếu bạn thực hiện phương pháp này và kết hợp với một thư viện như Marten, thì việc lưu trữ, tải và tìm các tổng hợp của bạn hầu như không cần viết code. Do tuân thủ ACID, bạn có thể sửa đổi và lưu nhiều tập hợp trong cùng một giao dịch, điều này không thể thực hiện được trong nhiều cơ sở dữ liệu tài liệu.
Sử dụng cơ sở dữ liệu quan hệ cũng giúp đơn giản hóa việc quản lý cơ sở hạ tầng của bạn, vì bạn vẫn có thể dựa vào các công cụ quen thuộc để sao lưu và giám sát.
Điều quan trọng hơn, mô hình miền của bạn không bị hạn chế bởi khả năng ORM của bạn.
Sử dụng Marten
Thêm Marten vào dự án
Như thường lệ, chúng ta bắt đầu bằng cách thêm phụ thuộc Marten vào dự án của mình từ NuGet thông qua Package Manager Console.
Install-Package Marten
Điều tiếp theo chúng ta cần làm là thêm một chuỗi kết nối vào cơ sở dữ liệu PostgreSQL của chúng ta vào file appsettings.json.
{
"ConnectionStrings": {
"PgConnection": "User ID=lab_user;Password=*****;Database=lab_netmicro_payments;Host=localhost;Port=5432"
}}
Chúng ta cũng cần cài đặt máy chủ cơ sở dữ liệu PostgreSQL.
Thiết lập Marten
Bây giờ chúng ta có thể thiết lập Marten. Chúng ta sẽ xem xét mã ví dụ được lấy từ PaymentService.
Trong giải pháp của chúng tôi, chúng tôi đã quyết định tách logic miền khỏi các chi tiết về lưu trữ dữ liệu và chúng tôi giới thiệu hai interface cho mục đích này.
public interface IPolicyAccountRepository
{
void Add(PolicyAccount policyAccount);
Task FindByNumber(string accountNumber);
}
Interface đầu tiên đại diện cho repository PolicyAccount. Ở đây chúng ta sử dụng mẫu repository như được mô tả trong cuốn sách DDD Blue Book của Eric Evans.
Repository của chúng ta cung cấp interface cho dữ liệu được lưu trữ để chúng tôi có thể sử dụng nó như một lớp thu thập đơn giản.
Lưu ý rằng chúng ta không tạo generic repository. Nếu chúng ta thiết kế theo hướng miền, thì repository phải là một phần của ngôn ngữ miền của chúng ta và chỉ nên hiển thị các hoạt động cần thiết của mã miền của chúng ta.
Interface thứ hai đại diện cho mẫu unit of work – một dịch vụ theo dõi các đối tượng được tải và cho phép chúng ta liên tục thay đổi.
public interface IDataStore : IDisposable
{
IPolicyAccountRepository PolicyAccounts { get; }
Task CommitChanges();
}
Interface IDataStore
cung cấp cho chúng ta quyền truy cập vào repository của mình. Vì vậy, chúng ta có thể thêm và truy xuất các đối tượng PolicyAccount từ nó và nó cho phép chúng ta lưu trữ các thay đổi vào cơ sở dữ liệu.
Hãy xem chúng ta có thể triển khai các interface này bằng cách sử dụng Marten như thế nào. Nhưng trước khi bắt đầu, chúng ta phải tìm hiểu về hai khái niệm cơ bản trong Marten: DocumentStore và DocumentSession.
DocumentStore đại diện cho cấu hình của kho tài liệu của chúng ta. Nó lưu trữ dữ liệu cấu hình như chuỗi kết nối, thiết lập serialize, tùy chỉnh lược đồ, thông tin ánh xạ.
DocumentSession đại diện cho đơn vị làm việc của chúng ta. Nó chịu trách nhiệm mở và quản lý kết nối cơ sở dữ liệu, thực thi các câu lệnh SQL đối với cơ sở dữ liệu, tải tài liệu, theo dõi các tài liệu đã tải và cuối cùng, lưu các thay đổi trở lại cơ sở dữ liệu.
Đầu tiên, bạn tạo một instance của DocumentStore, sau đó bạn có thể yêu cầu nó tạo một instance của DocumentSession và cuối cùng bạn có thể sử dụng DocumentSession để tạo, tải, sửa đổi và lưu trữ tài liệu trong cơ sở dữ liệu.
Có ba cách triển khai DocumentSession:
- Lightweight Session – một phiên không theo dõi các thay đổi.
- Standard Session – một phiên có theo dõi bản đồ nhận dạng, nhưng không có theo dõi thay đổi.
- Dirty Tracked Session – một phiên có bản đồ nhận dạng và theo dõi thay đổi.
Theo dõi thay đổi được triển khai dưới dạng so sánh giữa JSON được tải ban đầu từ cơ sở dữ liệu và JSON được tạo từ tập hợp của bạn, vì vậy bạn phải biết về hiệu suất và chi phí bộ nhớ. Trong mã của chúng tôi, chúng tôi sẽ sử dụng Lightweight Session.
Bạn có thể tìm hiểu thêm về nó từ tài liệu chính thức của Marten.
Bây giờ chúng ta đã biết những điều cơ bản. Chúng ta có thể tạo lớp MartenInstaller sẽ được sử dụng trong lớp Startup để khởi tạo và kết nối tất cả các phần cần thiết.
public static class MartenInstaller
{
public static void AddMarten(this IServiceCollection services, string cnnString)
{
services.AddSingleton(CreateDocumentStore(cnnString));
services.AddScoped<Domain.IDataStore, MartenDataStore>();
}
private static IDocumentStore CreateDocumentStore(string cn)
{
return DocumentStore.For(_ =>
{
_.Connection(cn);
_.DatabaseSchemaName = "payment_service";
_.Serializer(CustomizeJsonSerializer());
_.Schema.For().Duplicate(t => t.PolicyNumber,pgType: "varchar(50)", configure: idx => idx.IsUnique = true);
});
}
private static JsonNetSerializer CustomizeJsonSerializer()
{
var serializer = new JsonNetSerializer();
serializer.Customize(_ =>
{
_.ContractResolver = new ProtectedSettersContractResolver();
_.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
});
return serializer;
}
}
Phương thức chính ở đây là CreateDocumentStore. Nó tạo ra một instance của kho lưu trữ tài liệu và cấu hình nó.
DocumentStore.For(_ =>
{
_.Connection(cn); // 1
_.DatabaseSchemaName = "payment_service"; //2
_.Serializer(CustomizeJsonSerializer()); //3
_.Schema.For().Duplicate(t => t.PolicyNumber, pgType: "varchar(50)", configure: idx => idx.IsUnique = true); //4
});
Giải thích đoạn code trên:
- Cung cấp chuỗi kết nối đến cơ sở dữ liệu Postgresql.
- Tùy chỉnh tên lược đồ (nếu chúng ta không làm điều này, các bảng để lưu trữ tài liệu của chúng ta sẽ được tạo trong lược đồ công khai).
- Tùy chỉnh JsonSerializer, để nó có thể serialize các thuộc tính protected (điều này rất quan trọng, chúng ta đang cố gắng thiết kế các tập hợp theo quy tắc DDD, chúng ta không muốn để lộ trạng thái bên trong với bộ thiết lập công khai) và xử lý các tham chiếu vòng giữa các đối tượng.
- Chúng tôi thêm trường “duplicate” cho thuộc tính PolicyNumber. Ở đây, chúng tôi yêu cầu Marten không chỉ lưu trữ PolicyNumber như một phần tập hợp trong tài liệu JSON được serialize, mà còn tạo một cột riêng biệt và là chỉ mục duy nhất để tìm kiếm nhanh hơn. Chúng tôi làm điều này vì chúng tôi muốn nhanh chóng tìm thấy các tài khoản cho PolicyNumber nhất định.
Thông tin chi tiết về tùy chỉnh lược đồ và ánh xạ sẽ được cung cấp sau trong bài đăng này.
Hãy xem cách triển khai interface IDataStore:
public class MartenDataStore : IDataStore
{
private readonly IDocumentSession session;
public MartenDataStore(IDocumentStore documentStore)
{
session = documentStore.LightweightSession();
PolicyAccounts = new MartenPolicyAccountRepository(session);
}
public IPolicyAccountRepository PolicyAccounts { get; }
public async Task CommitChanges()
{
await session.SaveChangesAsync();
}
//...
}
Trong phương thức khởi tạo, chúng ta mở phiên tài liệu. Chúng ta sẽ đóng nó khi instance của lớp được xử lý (triển khai IDisposable được bỏ qua ở đây, nhưng bạn có thể kiểm tra mã hoàn chỉnh trên GitHub).
Phương thức CommitChanges sử dụng phương thức SaveChangesAsync của lớp DocumentSession. Ở đây chúng ta sử dụng API không đồng bộ của Marten, nhưng bạn cũng có thể sử dụng phiên bản đồng bộ nếu bạn thích.
Việc triển khai interface IPolicyAccountRepository rất đơn giản.
public class MartenPolicyAccountRepository : IPolicyAccountRepository
{
private readonly IDocumentSession documentSession;
public MartenPolicyAccountRepository(IDocumentSession documentSession)
{
this.documentSession = documentSession;
}
public void Add(PolicyAccount policyAccount)
{
this.documentSession.Insert(policyAccount);
}
public async Task FindByNumber(string accountNumber)
{
return await this.documentSession
.Query()
.FirstOrDefaultAsync(p => p.PolicyNumber == accountNumber);
}
}
Chúng ta chấp nhận tham chiếu để mở phiên tài liệu trong phương thức khởi tạo. Phương thức Add sử dụng phương thức Insert của lớp DocumentSession để thêm tài liệu mới trong Unit of Work. Tài liệu sẽ được lưu trong cơ sở dữ liệu khi phương thức CommitChanges được gọi. Phương thức CommitChanges sẽ gọi phương thức SaveChanges trên phiên tài liệu cơ bản.
Phương thức FindByNumber thú vị hơn vì nó cho thấy rằng bạn có thể sử dụng LINQ để xây dựng các truy vấn đối với các tài liệu được lưu trữ trong cơ sở dữ liệu. Trong trường hợp của chúng ta, đây là truy vấn rất đơn giản để tìm kiếm tài khoản chính sách bằng số tài khoản. Chúng tôi sẽ mô tả chi tiết hơn các khả năng của Marten LINQ trong bài đăng này.
Tùy chỉnh giản đồ và ánh xạ
Marten sẽ tạo một bảng cho từng kiểu .NET của tập hợp mà bạn muốn lưu vào cơ sở dữ liệu. Marten cũng sẽ tạo một chức năng cơ sở dữ liệu “upsert” cho mỗi bảng.
Theo mặc định, các bảng được tạo trong lược đồ public và được đặt tên bằng sự ghép nối của tiền tố “mt_doc_” và tên lớp của bạn. Trong trường hợp của chúng ta có lớp PolicyAccount, vì vậy Marten đã tạo bảng mt_doc_policyaccount.
Có nhiều tùy chọn tùy chỉnh khác nhau mà bạn có thể sử dụng.
Bạn có thể chỉ định lược đồ cơ sở dữ liệu sẽ được sử dụng. Trong trường hợp của chúng tôi, chúng tôi muốn tất cả các bảng được tạo như một phần của lược đồ “payment_service”.
var store = DocumentStore.For(_ =>
{
_.DatabaseSchemaName = "payment_service";
}
//You can also specify schema for each table.
_.Storage.MappingFor(typeof(BillingPeriod)).DatabaseSchemaName = "billing";
Theo mặc định, Marten tạo một bảng với các cột cho id, dữ liệu được serialize dưới dạng json và thêm một số cột siêu dữ liệu: ngày sửa đổi lần cuối, tên kiểu .NET, phiên bản (dùng cho tối ưu concurrency) và cột đánh dấu dữ liệu đã xóa.
Marten yêu cầu lớp của bạn phải có thuộc tính sẽ được ánh xạ tới khóa chính. Theo mặc định, Marten sẽ tìm một thuộc tính có tên: id, Id hoặc ID. Bạn có thể thay đổi nó bằng cách chú thích một trong các thuộc tính của lớp bằng attribute [Identity] hoặc tùy chỉnh ánh xạ trong mã khởi tạo kho tài liệu.
var store = DocumentStore.For(_ =>
{
_.Schema.For.Identity(x => x.MyId);
}
Bạn cũng có thể tùy chỉnh chiến lược tạo id. Ví dụ, bạn có thể chọn sử dụng CombGuid (hướng dẫn tuần tự).
_.Schema.For().IdStrategy(new CombGuidIdGeneration());
Nếu bạn muốn cải thiện hiệu suất truy vấn, bạn có thể yêu cầu Marten tạo chỉ mục và các trường trùng lặp.
Tùy chọn đầu tiên của bạn là sử dụng chỉ mục được tính toán. Trong ví dụ dưới đây, chúng tôi đã tạo một chỉ mục về họ và tên của chủ sở hữu, vì vậy việc tìm kiếm theo hai trường này sẽ nhanh hơn.
_.Schema.For().Index(x => x.Owner.FirstName);
_.Schema.For().Index(x => x.Owner.LastName);
Lưu ý rằng các chỉ mục được tính toán sẽ không hoạt động với các trường DateTime và DateTimeOffset.
Tùy chọn thứ hai là giới thiệu cái gọi là các trường trùng lặp. Chúng tôi sử dụng phương pháp này để tối ưu hóa việc tìm kiếm tài khoản theo mã chính sách tương ứng.
_.Schema.For().Duplicate(t => t.PolicyNumber, pgType: "varchar(50)", configure: idx => idx.IsUnique = true);
Ở đây chúng tôi yêu cầu Marten thêm trường bổ sung cho mã chính sách kiểu varchar(50) với unique index. Bằng cách này, Marten sẽ lưu mã chính sách không chỉ như một phần của dữ liệu JSON, mà còn lưu nó vào một cột riêng biệt, có unique index, vì vậy việc tìm kiếm trên đó sẽ rất nhanh.
Bạn có thể bật tối ưu concurrency cho kiểu nhất định như sau:
_.Schema.For().UseOptimisticConcurrency(true);
Có nhiều tùy chọn khác như full text index, foreign key cho phép chúng tôi liên kết hai tập hợp, gin / gist index.
Lưu tập hợp
Với IDataStore và IPolicyAccountRepository, việc lưu trữ tập hợp PolicyAccount của chúng ta thật dễ dàng.
Đây là mã ví dụ tạo tài khoản mới và lưu nó trong cơ sở dữ liệu.
public async Task Handle(PolicyCreated notification, CancellationToken cancellationToken)
{
var policy = new PolicyAccount(notification.PolicyNumber, policyAccountNumberGenerator.Generate());
using (dataStore)
{
dataStore.PolicyAccounts.Add(policy);
await dataStore.CommitChanges();
}
}
Như bạn có thể thấy, không cần ánh xạ dưới bất kỳ hình thức nào (mã hoặc thuộc tính) hoặc cấu hình để lưu và tải các đối tượng miền của bạn.
Các yêu cầu duy nhất mà các lớp của bạn phải đáp ứng là:
- Lớp của bạn phải có thể serialize thành JSON (bạn có thể kiểm tra cấu hình JSON Serializer để làm cho các lớp của bạn serialize / deserialize đúng cách).
- Lớp của bạn phải có trường hoặc thuộc tính định danh. Thuộc tính định danh sẽ được sử dụng làm giá trị cho khóa chính. Tên Trường / Thuộc tính phải là id hoặc Id hoặc ID, nhưng bạn có thể ghi đè quy tắc này bằng cách sử dụng attribute [Identity] hoặc tùy chỉnh ánh xạ trong mã. Các kiểu dữ liệu sau có thể được sử dụng làm mã định danh: string, Guid, CombGuid (hướng dẫn serialize), int, long hoặc lớp tùy chỉnh. Đối với int và long Marten sử dụng HiLo generator. Marten đảm bảo mã định danh được đặt trong IDocumentSession.Store.
Marten cũng hỗ trợ tối ưu concurrency. Tính năng này có thể được kích hoạt trên cơ sở từng loại tài liệu. Để bật tối ưu concurrency cho lớp của mình, bạn có thể thêm attribute [UseOptimisticConcurrency] vào lớp của mình hoặc tùy chỉnh cấu hình giản đồ.
Tải tập hợp
Việc tải các tập hợp cũng rất đơn giản.
public async Task Handle(GetAccountBalanceQuery request, CancellationToken cancellationToken)
{
var policyAccount = await dataStore.PolicyAccounts.FindByNumber(request.PolicyNumber);
if (policyAccount == null)
{
throw new PolicyAccountNotFound(request.PolicyNumber);
}
return new GetAccountBalanceQueryResult
{
Balance = new PolicyAccountBalanceDto
{
PolicyNumber = policyAccount.PolicyNumber,
PolicyAccountNumber = policyAccount.PolicyAccountNumber,
Balance = policyAccount.BalanceAt(DateTimeOffset.Now)
}
};
}
Truy vấn
Marten cung cấp hỗ trợ LINQ. Ví dụ truy vấn đơn giản tìm kiếm tài khoản chính sách bằng mã chính sách:
session.Query<PolicyAccount>().Where(p => p.PolicyNumber == "12121212")
Ví dụ truy vấn kết hợp nhiều tiêu chí với các toán tử logic:
session.Query<PolicyAccount>().Where(p => p.PolicyNumber == "12121212" && p.PolicyAccountNumber != "32323232323")
Ví dụ tìm kiếm dựa trên các tập hợp con của tập hợp của bạn và tìm kiếm các tài khoản có Entries có số tiền bằng 200:
var accounts = session.Query()
.Where(p => p.Entries.Any(_ => _.Amount == 200.0M))
.ToList()
Bạn cũng có thể tìm kiếm sâu bên trong hệ thống phân cấp đối tượng của mình. Ví dụ: nếu chúng ta đã lưu trữ dữ liệu chủ sở hữu tài khoản trên tài khoản chính sách, chúng ta có thể tìm kiếm tài khoản chính sách của một người cụ thể như sau:
var accounts = session.Query()
.Where(p => p.Owner.Name.LastName == “Jones” && p.Owner.Name.FirstName == “Tim”))
.ToList()
Bạn có thể tìm kiếm chuỗi sử dụng phương thức như: StartsWith, EndsWith và Contains.
session.Query<PolicyAccount>().Where(p => p.PolicyNumber.EndsWith("009898"))
Bạn có thể tính Count, Min, Max, Average và Sum trên các thuộc tính tập hợp của mình.
session.Query().Max(p => p.PolicyAccountNumber)
Bạn có thể sắp xếp kết quả và sử dụng Take / Skip để phân trang.
session.Query().Skip(10).Take(10).OrderBy(p => p.PolicyAccountNumber)
Ngoài ra còn có một phương thức tiện dụng là ToPagedList kết hợp của hai phương thức Skip và Take.
Nếu bạn gặp khó khăn trong việc tìm ra lý do tại sao truy vấn của bạn không hoạt động như bạn mong đợi, Marten cung cấp cho bạn khả năng xem trước truy vấn LINQ.
var query = session.Query().Where(p => p.PolicyNumber == "1223");
var cmd = query.ToCommand(FetchType.FetchMany);
var sql = cmd.CommandText;
Đoạn mã sau chuyển đổi truy vấn LINQ thành lệnh ADO.NET để bạn có thể kiểm tra câu lệnh truy vấn sql và các giá trị tham số.
Danh sách đầy đủ các toán tử được hỗ trợ có thể được tìm thấy tại đây.
Ngoài LINQ, bạn có thể sử dụng SQL để truy vấn tài liệu.
var user = session
.Query("select data from payment_service.mt_doc_policyaccount where data ->> 'PolicyAccountNumber' = 1221212")
.Single();
Bạn có thể chọn truy xuất JSON thô từ cơ sở dữ liệu.
var json = session.Json.FindById<PolicyAccount>(id);
Truy vấn tập hợp
Ngoài ra còn có chức năng nâng cao được gọi là truy vấn đã biên dịch. LINQ rất tuyệt và hữu ích khi xây dựng các truy vấn nhưng nó đi kèm với một hiệu suất nhất định và chi phí sử dụng bộ nhớ.
Trong trường hợp bạn có truy vấn phức tạp và thường được thực thi thì đó là một ứng cử viên tốt để tận dụng các truy vấn đã biên dịch.
Với các truy vấn đã biên dịch, bạn tránh được chi phí phân tích cú pháp cây biểu thức LINQ trên mỗi lần thực thi truy vấn.
Các truy vấn được biên dịch là các lớp thực hiện interface ICompiledQuery<TDoc, TResult>.
Ví dụ lớp truy vấn tìm kiếm tài khoản chính sách bằng mã tài khoản.
public class FindAccountByNumberQuery : ICompiledQuery<PolicyAccount, PolicyAccount>
{
public string AccountNumber { get; set; }
public Expression<Func<IQueryable, PolicyAccount>> QueryIs()
{
return q => q.FirstOrDefault(p => p.PolicyAccountNumber == AccountNumber);
}
}
Phương thức chính ở đây là QueryIs. Phương thức này trả về biểu thức xác định truy vấn.
Lớp này có thể được sử dụng như thế này:
var account = session.Query(new FindAccountByNumberQuery { AccountNumber = "11121212" });
Bạn có thể đọc thêm về các truy vấn đã biên dịch tại đây.
Cập nhật dữ liệu
Patch API của Marten có thể được sử dụng để cập nhật các tài liệu hiện có trong cơ sở dữ liệu của bạn. Đối với một số trường hợp, điều này có thể hiệu quả hơn việc tải toàn bộ tài liệu vào bộ nhớ, tuần tự hóa nó, thay đổi, giải mã hóa và sau đó lưu lại vào cơ sở dữ liệu.
Patch API cũng có thể rất hữu ích khi sửa lỗi trong dữ liệu và để xử lý các thay đổi trong cấu trúc lớp của bạn.
Thiết kế của chúng ta sẽ không cố định mãi mãi. Theo thời gian, chúng ta sẽ thêm các thuộc tính mới vào các lớp của mình, thay đổi tham chiếu đơn giản thành một tập hợp hoặc ngược lại. Một số thuộc tính có thể được trích xuất và cấu trúc lại thành một lớp mới, một số thuộc tính có thể bị loại bỏ.
Khi làm việc với các bảng trong cơ sở dữ liệu quan hệ, chúng ta có các lệnh SQL DDL nổi tiếng như ALTER TABLE ADD / DROP COLUMN.
Khi làm việc với các tài liệu JSON, bằng cách nào đó chúng ta phải đối phó với tất cả các thay đổi, để các tài liệu hiện có vẫn có thể được tải và truy vấn khi các lớp tương ứng được thay đổi.
Hãy cố gắng sửa đổi lớp PolicyAccount của chúng ta và di chuyển dữ liệu hiện có trong cơ sở dữ liệu để nó luôn nhất quán.
Chúng tôi bắt đầu với PolicyAccount phải có thuộc tính đại diện cho họ và tên của chủ sở hữu tài khoản.
public class PolicyAccount
{
public Guid Id { get; protected set; }
public string PolicyAccountNumber { get; protected set; }
public string PolicyNumber { get; protected set; }
public string OwnerFirstName { get; protected set; }
public string OwnerName { get; protected set; }
public ICollection Entries { get; protected set; }
//…
}
Trong cơ sở dữ liệu, dữ liệu của chúng ta trông như thế này:
{
"Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7",
"$id": "2",
"Entries": [],
"OwnerName": "Jones",
"PolicyNumber": "POLICY_1",
"OwnerFirstName": "Tim",
"PolicyAccountNumber": "231232132131"
}
Như chúng ta có thể thấy OwnerName không phải là tên tốt nhất và chúng ta muốn đổi tên nó thành OwnerLastName.
Về phần C# thì cực kỳ dễ dàng vì hầu hết các IDE đều cung cấp khả năng tái cấu trúc lại tên. Hãy làm điều đó và sau đó sử dụng Patch API để sửa dữ liệu trong cơ sở dữ liệu
public void RenameProperty()
{
using (var session = SessionProvider.OpenSession())
{
session
.Patch(x => x.OwnerLastName == null)
.Rename("OwnerName", x => x.OwnerLastName);
session.SaveChanges();
}
}
Nếu bạn chạy phương thức này dữ liệu trong cơ sở dữ liệu bây giờ trông giống như sau:
{
"Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7",
"$id": "2",
"Entries": [],
"PolicyNumber": "POLICY_1",
"OwnerLastName": "Jones",
"OwnerFirstName": "Tim",
"PolicyAccountNumber": "231232132131"
}
Hãy thử một cái gì đó phức tạp hơn. Chúng ta quyết định trích xuất OwnerFirstName và OwnerLastName thành một lớp. Bây giờ mã C# của chúng ta trông giống như sau:
public class PolicyAccount
{
public Guid Id { get; protected set; }
public string PolicyAccountNumber { get; protected set; }
public string PolicyNumber { get; protected set; }
public string OwnerFirstName { get; protected set; }
public string OwnerLastName { get; protected set; }
public Owner Owner { get; protected set; }
public ICollection Entries { get; protected set; }
}
Chúng ta đã thêm một lớp mới với các thuộc tính FirstName và LastName. Bây giờ chúng ta sẽ sử dụng Patch API để sửa dữ liệu trong cơ sở dữ liệu.
public void AddANewProperty()
{
using (var session = SessionProvider.OpenSession())
{
session
.Patch(x=>x.Owner.LastName==null)
.Duplicate(x => x.OwnerLastName, w => w.Owner.LastName);
session
.Patch(x=>x.Owner.FirstName==null)
.Duplicate(x => x.OwnerFirstName, w => w.Owner.FirstName);
session.SaveChanges();
}
}
Và dữ liệu trong cơ sở dữ liệu của chúng ta sẽ như sau:
{
"Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7",
"$id": "2",
"Owner": {
"LastName": "Jones",
"FirstName": "Tim"
},
"Entries": [],
"PolicyNumber": "POLICY_1",
"OwnerLastName": "Jones",
"OwnerFirstName": "Tim",
"PolicyAccountNumber": "231232132131"
}
Bây giờ là lúc để dọn dẹp. Chúng ta phải xóa các thuộc tính OwnerFirstName và OwnerLastName không sử dụng khỏi mã C# và khỏi dữ liệu trong cơ sở dữ liệu.
public void RemoveProperty()
{
using (var session = SessionProvider.OpenSession())
{
session
.Patch(x=>x.Owner!=null)
.Delete("OwnerLastName");
session
.Patch(x=>x.Owner!=null)
.Delete("OwnerFirstName");
session.SaveChanges();
}
}
Dữ liệu trong cơ sở dữ liệu bây giờ trông như thế này. OwnerFirstName và OwnerLastName đã biến mất.
{
"Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7",
"$id": "2",
"Owner": {
"LastName": "Jones",
"FirstName": "Tim"
},
"Entries": [],
"PolicyNumber": "POLICY_1",
"PolicyAccountNumber": "231232132131"
}
Patch API cung cấp rất nhiều thao tác. Bạn có thể đọc thêm về nó ở đây.
Patch API yêu cầu bạn cài đặt công cụ PLV8 cho PostgreSQL.
Ngoài Patch API của Marten, bạn luôn có thể sử dụng toàn bộ sức mạnh của PostgreSQL, nó cung cấp cho bạn các hàm để làm việc với kiểu JSON và kết hợp nó với khả năng sử dụng JavaScript như ngôn ngữ của các hàm / thủ tục cơ sở dữ liệu được cung cấp bởi công cụ PLV8.
Trên thực tế, những gì Patch API tạo ra là các hàm được viết bằng JavaScript và được thực thi trong cơ sở dữ liệu bằng công cụ PLV8.
Ưu và nhược điểm của Marten
Ưu điểm của Marten
- Tốt nhất của cả hai thế giới: ACID và SQL hỗ trợ cơ sở dữ liệu quan hệ dễ sử dụng và phát triển cơ sở dữ liệu tài liệu.
- Hỗ trợ ACID cho phép bạn lưu nhiều tài liệu từ nhiều bảng khác nhau trong một giao dịch, điều này không được hầu hết các cơ sở dữ liệu tài liệu hỗ trợ.
- Làm việc với tài liệu cho phép bạn lưu và tải tài liệu của mình mà không cần phải ánh xạ xác định giữa mô hình đối tượng và mô hình cơ sở dữ liệu của bạn vốn được yêu cầu khi sử dụng cơ sở dữ liệu quan hệ. Điều này dẫn đến sự phát triển nhanh hơn, đặc biệt là trong giai đoạn đầu của quá trình phát triển khi bạn không phải lo lắng về những thay đổi chương trình của mình.
- Hỗ trợ rộng rãi cho các truy vấn LINQ mang lại cho người dùng Entity Framework và NHibernate trải nghiệm quen thuộc.
- Khả năng sử dụng như một kho lưu trữ tài liệu và một cửa hàng sự kiện.
- Khả năng nhanh chóng thiết lập / chia nhỏ dữ liệu cho bạn kiểm tra đơn vị / tích hợp.
- Hỗ trợ Bulk operations.
- Patch API để cập nhật các tài liệu hiện có.
- Khả năng sử dụng SQL khi truy vấn LINQ không thể thực hiện được hoặc không hoạt động.
- Dễ dàng sử dụng cơ sở dữ liệu thực trong các bài kiểm tra tích hợp của bạn. Việc thiết lập cơ sở dữ liệu, điền dữ liệu ban đầu và sau đó dọn dẹp nó rất đơn giản và nhanh chóng.
- Hỗ trợ Multitenancy.
- Hỗ trợ truy vấn được biên dịch và truy vấn hàng loạt.
- DocumentSession có khả năng tham gia vào DocumentScope quản lý giao dịch.
Nhược điểm của Marten
- Chỉ hoạt động với PostgreSQL.
- Di chuyển dữ liệu với Patch API yêu cầu nhiều công việc hơn và học hỏi những điều mới.
- Hỗ trợ hạn chế cho việc tìm kiếm trong tập hợp con.
- Không thực sự phù hợp để báo cáo và truy vấn đặc biệt.
Tóm lược
Có nhiều tùy chọn để lựa chọn khi thiết kế chiến lược truy cập dữ liệu cho microservices của bạn. Có các tùy chọn khác ngoài Entity Framework hoặc SQL thủ công.
Marten là một thư viện hoàn chỉnh với nhiều tính năng hữu ích và hỗ trợ LINQ tốt. Nếu bạn đang nhắm mục tiêu đến cơ sở dữ liệu PostgreSQL và sử dụng phương pháp thiết kế hướng theo miền để chia mô hình miền của bạn thành các tập hợp nhỏ, thì Marten rất đáng để thử.
Nó cũng có thể là công cụ rất hữu ích trong giai đoạn thiết kế và khám phá hoặc khi xây dựng các nguyên mẫu cho phép bạn nhanh chóng phát triển mô hình miền của mình và có thể duy trì và truy vấn dữ liệu của bạn.
Bài viết gốc.