Tìm hiểu về Power Query / M Language (Part 10): Kiểu dữ liệu – List, Record (tiếp theo)
Tiếp theo với phần tìm hiểu về kiểu dữ liệu danh sách (list) và bản ghi (record) chúng ta tiếp tục tìm hiểu về kiểu bản ghi (record)
Bản ghi (record)
Một bản ghi (record) cho phép một tập hợp các trường đã đặt tên được nhóm lại thành một đơn vị.
[FirstName = "Data", LastName = "Mashup", Birthdate = #date(2021, 9, 10)]
Về mặt kỹ thuật, một bản ghi duy trì thứ tự các trường của nó. Tuy nhiên, chúng ta sẽ thấy trong các ví dụ dưới, thứ tự trường không được xem xét khi so sánh các bản ghi, vì vậy phần lớn việc bảo toàn thứ tự trường này là một sự thuận tiện cho con người (ví dụ: các trường sẽ được xuất trên màn hình theo đúng thứ tự bạn đã xác định chúng, giúp bạn xác định vị trí các mục dữ liệu quan tâm một cách trực quan hơn).
Một bản ghi có thể định nghĩa trống, không chứa trường nào.
[ ]
So sánh bằng được xác định bởi tên trường và giá trị. Vị trí trường không được xem xét.
[ a = 1, b = 2] = [a = 1, b = 2] // true
[ a = 1, b = 2] = [b = 2, a = 1 ] // true -- tên và giá trị trường giống nhau, mặc dù thứ tự khác nhau
[ a = 1 ] = [ A = 1 ] // false -- các tên trường khác nhau
[ a = 1 ] = [ a = 2 ] // false -- các tên trường giống nhau nhưng giá trị khác nhau
[ a = 1 ] <> [ A = 1 ] // true
Các bản ghi có thể được hợp nhất ( merged ) với toán tử kết hợp &.
[ a = 1 ] & [ b = 2 ] // [ a = 1, b = 2]
Nếu có cùng một tên trường trong cả hai đầu vào hợp nhất, thì giá trị được liên kết với trường từ bên phải sẽ được sử dụng.
[ a = 1 ] & [ a = 10 ] // [ a = 10 ]
Truy cập các trường trong bản ghi
Cách danh sách sử dụng {index} để truy cập các mục danh sách? Với các bản ghi, một cái gì đó tương tự cũng được sử dụng – toán tử tra cứu, bao gồm tên trường bên trong dấu ngoặc vuông: MyRecord[FieldName]
Giả sử
MyRecord = [Part = 3555, Description = "PowerBI", Price = 10.29, Internal Cost = 8.50]
các biểu thức sau sẽ trả về các giá trị:
MyRecord[Part] // 3555
MyRecord[Description] // "PowerBI"
MyRecord[Price] // 10.29
MyRecord[Internal Cost] // 8.50
Tương tự với danh sách, thêm dấu hỏi ‘?’ toán tử tra cứu sẽ thay đổi hành vi không tìm thấy của nó từ lỗi thành trả về null (về mặt kỹ thuật, điều này được gọi là “thực hiện một lựa chọn trường tùy chọn”).
MyRecord[NonExistentField] // error - Expression.Error: Không tìm thấy 'NonExistingField' đã lưu của bản ghi.
MyRecord[NonExistentField]? // null
Trong một bản ghi, biểu thức cho một giá trị một trường trường có thể tham chiếu đến các trường khác.
[
FirstName = "Data",
LastName = "Mashup",
FullName = FirstName & " " & LastName
]
Biểu thức của một trường thậm chí có thể tự tham chiếu nếu tên của nó được tiếp tục sử dụng, bởi toán tử xác định phạm vi (@).
Hành vi này có vẻ không trực quan trong ngữ cảnh của trường chứa giá trị dữ liệu. Tuy nhiên, khả năng tự tham chiếu có ích khi giá trị là một hàm vì nó cho phép hàm được đệ quy.
[
AddOne = (x) => if x > 0 then 1 + @AddOne(x - 1) else 0,
AddOneFiveTimes = AddOne(5)
][AddOneFiveTimes] // 5
Phép chiếu
Ngoài dấu ngoặc vuông được sử dụng để chọn các trường bản ghi, chúng cũng có thể được sử dụng để thực hiện phép chiếu bản ghi – nghĩa là định hình lại bản ghi để chứa ít trường hơn. Dưới đây là một số ví dụ (giả sử Source = [FieldA = 10, FieldB = 20, FieldC = 30]):
Source[[FieldA], [FieldB]] // [ FieldA = 10, FieldB = 20 ] -- FieldC đã bị xóa
Source[[FieldC]] // [ FieldC = 30 ] -- ngoại trừ FieldC, tất cả các trường khác đã bị xóa
Tương tự khi dấu [ ] được sử dụng để chọn trường, với phép chiếu, việc tham chiếu đến trường không tồn tại sẽ gây ra lỗi. Tuy nhiên, nếu dấu hỏi ‘?’ được thêm vào, mọi trường không tồn tại được tham chiếu bởi biểu thức đầu ra với giá trị được đặt thành null.
Source[[FieldA], [FieldD]] // error - Expression.Error - Không tìm thấy trường 'FieldD' của bản ghi.
Source[[FieldA], [FieldD]]? // [ FieldA = 10, FieldD = null]
Quy tắc trích dẫn
Trong cú pháp dấu ngoặc vuông [ ], lựa chọn và chiếu bản ghi, tên trường có các quy tắc trích dẫn dễ dàng. Thông thường, để bắt đầu một mã định danh bằng một số, để sử dụng từ khóa ngôn ngữ M làm định danh hoặc để khoảng trắng ở giữa mã định danh, mã định danh phải được trích dẫn. Tuy nhiên, trong những trường hợp này, trích dẫn là tùy chọn đối với các định danh tên trường bên trong dấu ngoặc vuông.
Bên ngoài dấu ngoặc vuông, định danh trường Street Address cần trích dẫn vì nó chứa một khoảng trắng và try cần trích dẫn vì đó là một từ khóa. Dưới đây, bên trong dấu ngoặc vuông, trích dẫn các số nhận dạng này chỉ là tùy chọn:
[#"try" = true, #"Street Address" = "377 CMT8 St."]
[try = true, Street Address = "377 CMT8 St."] // có hiệu lực tương tự với dòng trên
MyRecord[#"Street Address"]
MyRecord[Street Address] // có hiệu lực tương tự với dòng trên
MyRecord[#"try"]
MyRecord[try] // có hiệu lực tương tự với dòng trên
Tuy nhiên, lưu ý rằng, M giả sử khoảng trắng xuất hiện ở đầu hoặc cuối của tên trường không được trích dẫn có thể bị bỏ qua và do đó loại trừ nó khỏi tên trường. Nếu vì lý do nào đó, bạn muốn khoảng trắng ở đầu hoặc cuối là một phần của tên trường, bạn sẽ cần phải trích dẫn nó.
MyRecord[ Street Address ] // truy cập vào trường "Street Address"
MyRecord[#" Street Address "] // truy cập vào trường " Street Address "
Đánh giá lười biếng ( Lazy code)
Giống như kiểu danh sách, bản ghi cũng lười biếng. Nếu một giá trị không cần thiết, nó sẽ không được đánh giá (kiểm tra).
[Price = 10, Quantity = error "help"][Price] // 10
Ở ví dụ trên, vì Quantity là không cần thiết, nên giá trị của nó không được đánh giá. Vì giá trị của nó không được đánh giá nên không có lỗi nào được đưa ra.
Khi một trường được đánh giá lần đầu tiên, giá trị hoặc lỗi kết quả được xác định làm giá trị cho trường đó. Biểu thức của trường chỉ được thực thi một lần. Đầu ra của nó sau đó được lưu vào bộ nhớ đệm. Giá trị hoặc lỗi đã lưu trong bộ nhớ cache được trả về mỗi lần truy cập trường lần tiếp theo.
[ Price = GetValueFromRemoteServer() ]
Hãy tưởng tượng rằng lần đầu tiên Price được truy cập, máy chủ trả về 10. Sau đó, trong khi mashup của bạn vẫn đang chạy, trường Price của bản ghi được truy cập lại. Có lẽ vào thời điểm này, việc gọi GetValueFromRemoteServer() sẽ trả về 11. Tuy nhiên, phương thức đó không được thực thi lại. Thay vào đó, giá trị được lưu trong bộ nhớ cache khi trường được truy cập lần đầu tiên (10) sẽ được trả về.
Thay vào đó, nếu lần đầu tiên Price được truy cập, GetValueFromRemoteServer() đã phát sinh lỗi do trục trặc trong giao tiếp tạm thời, thì lỗi tương tự đó sẽ xuất hiện lại mỗi lần tiếp theo Price được truy cập, ngay cả khi vào thời điểm truy cập tiếp theo, lỗi đó xảy ra được giải quyết và GetValueFromRemoteServer() sẽ trả về một giá trị không lỗi nếu nó được gọi.
Việc sửa chữa giá trị này (hoặc bộ nhớ đệm) cung cấp tính nhất quán. Nhờ đó, bạn biết rằng giá trị của một trường sẽ luôn giống nhau trong suốt quá trình thực thi mashup của bạn.
Bộ nhớ đệm giá trị không được chia sẻ giữa các bản ghi, ngay cả khi bản ghi có các trường và biểu thức giá trị trường giống hệt nhau. Nếu mã của bạn khiến bản ghi [Price = GetValueFromRemoteServer ()] được tạo hai lần và Giá được truy cập trên cả hai trường hợp, mỗi trường hợp sẽ gọi riêng GetValueFromRemoteServer() một lần và lưu vào bộ nhớ cache giá trị được trả về. Nếu giá trị trả về khác nhau giữa hai lời gọi, hai bản ghi sẽ có giá trị khác nhau cho Price.
Nếu bản ghi bạn đang làm việc được gán cho một biến, mỗi lần bạn truy cập vào biến đó, bạn sẽ truy cập vào cùng một bản ghi. Tuy nhiên, nếu thay vào đó, bạn truy cập một bản ghi nhiều lần bằng cách gọi một biểu thức truy xuất nó từ một nguồn bên ngoài (ví dụ: cơ sở dữ liệu hoặc dịch vụ web), về mặt kỹ thuật, mỗi lần truy xuất có thể trả về một bản ghi khác nhau. Nếu điều quan trọng là phải đảm bảo rằng bạn luôn làm việc với cùng một bản ghi, hãy truy xuất bản ghi đó một lần sau đó lưu nó vào một biến hoặc trong trường hợp là danh sách bản ghi. Tìm hiểu thêm danh sách đệm.
Thư viện hàm hỗ trợ
Trong thư viện chuẩn, bạn sẽ tìm thấy một số hàm để làm việc với các bản ghi, bao gồm các phương pháp thêm, đổi tên, sắp xếp lại và xóa các trường cũng như để chuyển đổi các giá trị của trường. Ngoài ra còn có một phương thức trả về danh sách tên trường của bản ghi (với thứ tự trường được giữ nguyên) và một phương thức tương tự trả về giá trị trường.
Toán tử động
Ở trên, tôi đã sử dụng toán tử tra cứu để truy cập các giá trị trường theo tên được mã hóa cứng. Thay vào đó, điều gì sẽ xảy ra nếu chúng tôi muốn sử dụng logic lập trình để chọn trường để truy cập? Cách sau không hoạt động vì tên trường bên trong dấu ngoặc vuông phải là chuỗi, tham chiếu biến không được phép.
let
Item = [Name = "Data Mashup", Wholesale Price = 5, Retail Price = 10],
PriceToUse = "Wholesale Price"
in
Item[PriceToUse] // không hoạt động - không trả về giá trị của trường "Wholesale Price"
Để giải quyết tình huống này, sử dụng thư viện tiêu chuẩn để giải cứu. Record.Field là tương đương tra cứu động của toán tử tra cứu. Record.FieldOrDefault hoạt động giống như một toán tử tra cứu động theo sau là dấu chấm hỏi ‘?’, với phần bổ sung là tùy chọn cho phép bạn chỉ định giá trị sẽ được trả về nếu tên trường không tồn tại
Record.Field(Item, PriceToUse) // kết quả 5
Thay vào đó, nếu PriceToUse được đặt thành “Sale Price” (giá trị không tương ứng với tên trường), thì:
Record.Field(Item, PriceToUse) // error - Expression.Error: Không tìm thấy trường 'Sale Price' của bản ghi.
Record.FieldOrDefault(Item, PriceToUse) // kết quả null
Record.FieldOrDefault(Item, PriceToUse, 0) // kết quả 0
Tương tự, nếu bạn muốn tự động thực hiện phép chiếu, Record.SelectFields là lựa chọn. Ngoài ra còn có các thư viện tiêu chuẩn để loại bỏ các trường (thay vì chiếu bằng cách liệt kê các trường mong muốn, chỉ định các trường không mong muốn và một bản ghi mới chứa tất cả các trường khác sẽ được trả về) và sắp xếp lại các trường (hữu ích trong một số trường hợp thứ tự trường).
Cú pháp let in và record
Bất ngờ, về bản chất, một biểu thức let là cú pháp cho một biểu thức bản ghi ngầm định.
let
A = 1,
B = 2,
Result = A + B
in
Result
Tương đương với:
[
A = 1,
B = 2,
Result = A + B
][Result]
Thực tế này có nghĩa là những gì chúng ta biết về cách thức hoạt động của các bản ghi cũng áp dụng cho các biểu thức let và ngược lại.
Ví dụ: chúng ta biết rằng giá trị của trường bản ghi được tính ở lần truy cập đầu tiên sau đó được lưu vào bộ nhớ đệm. Vì let về bản chất là một biểu thức bản ghi, nên quy tắc bất biến này cũng áp dụng cho nó: biểu thức của biến let sẽ được đánh giá trong lần truy cập đầu tiên, sau đó giá trị của nó sẽ được lưu vào bộ nhớ đệm. Tuy nhiên, đối với biểu thức let, chúng ta biết rằng có một ngoại lệ đối với tính bất biến, điều này xảy ra khi tính năng trực tuyến (streaming) hoạt động. Ngoại lệ tương tự này cũng phải áp dụng cho các bản ghi… bởi vì let và record có cùng một hành vi.
Kết luận
Bạn có nhận thấy rằng bản ghi (record) sẽ rất tuyệt khi lưu giữ một hàng dữ liệu? Nếu bạn muốn lưu trữ nhiều bản ghi, mỗi bản ghi đại diện cho một hàng dữ liệu, trong một biến duy nhất, bạn có thể đặt các bản ghi đó trong một danh sách (list). Như thế có vẻ như chúng ta đang tiến gần đến kiểu bảng (table) và chúng ta sẽ tìm hiểu trong các bài viết sắp tới.