Cách tốt nhất để có được danh tính của hàng chèn?


1119

Cách tốt nhất để có IDENTITYđược hàng chèn là gì?

Tôi biết @@IDENTITYIDENT_CURRENTSCOPE_IDENTITYkhông hiểu những ưu và nhược điểm của mỗi loại.

Ai đó có thể vui lòng giải thích sự khác biệt và khi nào tôi nên sử dụng từng?


5
INSERT INTO Table1(fields...) OUTPUT INSERTED.id VALUES (...)hoặc phương thức cũ hơn: INSERT INTO Table1(fields...) VALUES (...); SELECT SCOPE_IDENTITY();bạn có thể lấy nó trong c # bằng ExecuteScalar ().
S.Serpooshan

4
Làm thế nào là tốt hơn so với các câu trả lời khác? (cũng - tại sao bạn không đăng bài này dưới dạng câu trả lời thay vì bình luận). Vui lòng viết lên một câu trả lời đầy đủ (và giải thích lý do tại sao đây là một lựa chọn tốt hơn so với câu trả lời - nếu phiên bản cụ thể, hãy nói như vậy).
Oded

nó giống như một bản tóm tắt ngắn ; D Câu trả lời được chấp nhận không đề cập đến cú pháp mệnh đề OUTPUT và thiếu một mẫu. Ngoài ra các mẫu trong các bài đăng khác không được sạch sẽ ...
S.Serpooshan

2
@saeederpooshan - sau đó chỉnh sửa nó. Bạn có thể làm điều đó, bạn biết không? Xem khi câu trả lời đã được đăng? Điều đó có trước OUTPUTmệnh đề trong SQL Server.
Oded

Câu trả lời:


1435
  • @@IDENTITYtrả về giá trị nhận dạng cuối cùng được tạo cho bất kỳ bảng nào trong phiên hiện tại, trên tất cả các phạm vi. Bạn cần phải cẩn thận ở đây , vì nó trên phạm vi. Bạn có thể nhận được một giá trị từ một kích hoạt, thay vì tuyên bố hiện tại của bạn.

  • SCOPE_IDENTITY()trả về giá trị nhận dạng cuối cùng được tạo cho bất kỳ bảng nào trong phiên hiện tại và phạm vi hiện tại. Nói chung những gì bạn muốn sử dụng .

  • IDENT_CURRENT('tableName')trả về giá trị nhận dạng cuối cùng được tạo cho một bảng cụ thể trong bất kỳ phiên và bất kỳ phạm vi nào. Điều này cho phép bạn chỉ định bảng nào bạn muốn giá trị từ đó, trong trường hợp hai bảng trên không hoàn toàn là thứ bạn cần ( rất hiếm ). Ngoài ra, như @ Guy Starbuck đã đề cập, "Bạn có thể sử dụng điều này nếu bạn muốn nhận giá trị IDENTITY hiện tại cho một bảng mà bạn chưa chèn bản ghi vào."

  • Các OUTPUTđiều khoản của INSERTtuyên bố sẽ cho phép bạn truy cập vào tất cả các hàng đã được chèn qua tuyên bố đó. Vì nó nằm trong phạm vi của tuyên bố cụ thể, nó đơn giản hơn các chức năng khác ở trên. Tuy nhiên, đó là một chút dài dòng hơn (bạn sẽ cần chèn vào một biến bảng / bảng tạm thời và sau đó truy vấn đó) và nó cho kết quả ngay cả trong một kịch bản lỗi trong đó câu lệnh được khôi phục. Điều đó nói rằng, nếu truy vấn của bạn sử dụng một kế hoạch thực hiện song song, đây là phương pháp được bảo đảm duy nhất để nhận dạng (tắt tắt tính song song). Tuy nhiên, nó được thực thi trước khi kích hoạt và không thể được sử dụng để trả về các giá trị được kích hoạt.


48
lỗi được biết đến với SCOPE_IDENTITY () trả về giá trị sai: blog.sqlauthority.com/2009/03/24/... công việc xung quanh là không chạy INSERT trong một bộ xử lý đa song song kế hoạch hoặc sử dụng mệnh đề OUTPUT
KM.

3
Hầu như mỗi lần tôi muốn 'danh tính', tôi đều muốn biết (các) khóa của bản ghi tôi vừa chèn. Nếu đó là tình huống của bạn, bạn muốn sử dụng mệnh đề OUTPUT. Nếu bạn muốn một cái gì đó khác, áp dụng nỗ lực để đọc và hiểu phản ứng bdukes.
jerry

3
Với outputbạn không cần tạo bảng tạm thời để lưu trữ và truy vấn kết quả. Chỉ cần bỏ đi intomột phần của mệnh đề đầu ra và nó sẽ xuất chúng thành một tập kết quả.
spb

96
Để cứu người khác khỏi hoảng loạn, lỗi được đề cập ở trên đã được sửa trong Bản cập nhật tích lũy 5 cho Gói dịch vụ SQL Server 2008 R2 1.
GaTechThomas

1
@niico, tôi nghĩ rằng đề xuất này giống như trước đây, đó là OUTPUT"tốt nhất" miễn là bạn không sử dụng trình kích hoạt và đang xử lý lỗi, nhưng SCOPE_IDENTITYđơn giản nhất và rất hiếm khi xảy ra sự cố
bdukes

180

Tôi tin rằng phương pháp an toàn và chính xác nhất để truy xuất id được chèn sẽ sử dụng mệnh đề đầu ra.

ví dụ (lấy từ bài viết MSDN sau )

USE AdventureWorks2008R2;
GO
DECLARE @MyTableVar table( NewScrapReasonID smallint,
                           Name varchar(50),
                           ModifiedDate datetime);
INSERT Production.ScrapReason
    OUTPUT INSERTED.ScrapReasonID, INSERTED.Name, INSERTED.ModifiedDate
        INTO @MyTableVar
VALUES (N'Operator error', GETDATE());

--Display the result set of the table variable.
SELECT NewScrapReasonID, Name, ModifiedDate FROM @MyTableVar;
--Display the result set of the table.
SELECT ScrapReasonID, Name, ModifiedDate 
FROM Production.ScrapReason;
GO

3
Có, đây là phương pháp chính xác trong tương lai, chỉ sử dụng một trong những phương pháp khác nếu bạn không sử dụng SQL Server 2008 (chúng tôi đã bỏ qua năm 2005 vì vậy không chắc là OUTPUT có sẵn hay không)
HLGEM

1
@HLGEM Có một trang MSDN cho OUTPUTSQL Server 2005 , vì vậy có vẻ như đó chỉ là SQL Server 2000 và trước đó không có nó
bdukes

6
woohoo! OUTPUT CLAUSE đá :) Điều đó sẽ đơn giản hóa nhiệm vụ hiện tại của tôi. Không biết tuyên bố đó trước đây. Cảm ơn các bạn!
SwissCoder

8
Để có một ví dụ thực sự ngắn gọn để lấy ID được chèn, hãy xem: stackoverflow.com/a/10999467/2003325
Luke

Việc bạn sử dụng INTO với OUTPUT là một ý tưởng hay. Xem: blogs.msdn.microsoft.com/sqlprogrammability/2008/07/11/... (Từ một bình luận ở đây: stackoverflow.com/questions/7917695/... )
shlgug

112

Tôi đang nói điều tương tự như những người khác, vì vậy mọi người đều đúng, tôi chỉ đang cố gắng làm cho nó rõ ràng hơn.

@@IDENTITYtrả về id của điều cuối cùng được chèn bởi kết nối của khách hàng với cơ sở dữ liệu.
Hầu hết thời gian điều này hoạt động tốt, nhưng đôi khi một trình kích hoạt sẽ đi và chèn một hàng mới mà bạn không biết và bạn sẽ nhận được ID từ hàng mới này, thay vì một hàng bạn muốn

SCOPE_IDENTITY()giải quyết vấn đề này Nó trả về id của điều cuối cùng mà bạn đã chèn vào mã SQL mà bạn đã gửi đến cơ sở dữ liệu. Nếu kích hoạt đi và tạo thêm hàng, chúng sẽ không khiến giá trị sai được trả về. hoan hô

IDENT_CURRENTtrả về ID cuối cùng được chèn bởi bất kỳ ai. Nếu một số ứng dụng khác tình cờ chèn một hàng khác vào một thời điểm không may mắn, bạn sẽ nhận được ID của hàng đó thay vì một hàng của bạn.

Nếu bạn muốn chơi nó an toàn, luôn luôn sử dụng SCOPE_IDENTITY(). Nếu bạn gắn bó @@IDENTITYvà ai đó quyết định thêm trình kích hoạt sau này, tất cả mã của bạn sẽ bị hỏng.


64

Cách tốt nhất (đọc: an toàn nhất) để có được danh tính của một hàng mới được chèn là sử dụng outputmệnh đề:

create table TableWithIdentity
           ( IdentityColumnName int identity(1, 1) not null primary key,
             ... )

-- type of this table's column must match the type of the
-- identity column of the table you'll be inserting into
declare @IdentityOutput table ( ID int )

insert TableWithIdentity
     ( ... )
output inserted.IdentityColumnName into @IdentityOutput
values
     ( ... )

select @IdentityValue = (select ID from @IdentityOutput)

5
Phân cụm máy chủ SQL là một tính năng sẵn sàng cao và không có liên quan đến tính song song. Rất hiếm khi chèn các hàng đơn (trường hợp phổ biến nhất scope_identity()) để có được các kế hoạch song song. Và lỗi này đã được sửa hơn một năm trước câu trả lời này.
Martin Smith

Bạn có ý nghĩa gì bởi sự song song.
dùng1451111

@MartinSmith Khách hàng không sẵn sàng cho phép ngừng hoạt động trên cụm máy chủ của họ để cài đặt CU khắc phục sự cố này (không đùa), vì vậy giải pháp duy nhất là chúng tôi viết lại tất cả SQL để sử dụng outputthay vì scope_identity(). Tôi đã loại bỏ FUD về phân cụm trong câu trả lời.
Ian Kemp

1
Cảm ơn bạn, đây là ví dụ duy nhất tôi có thể tìm thấy cho thấy cách sử dụng giá trị từ đầu ra trong một biến thay vì chỉ xuất ra nó.
Sean Ray

26

Thêm vào

SELECT CAST(scope_identity() AS int);

đến cuối câu lệnh chèn sql của bạn, sau đó

NewId = command.ExecuteScalar()

sẽ lấy nó


18

Khi bạn sử dụng Entity Framework, bên trong nó sử dụng OUTPUTkỹ thuật để trả về giá trị ID mới được chèn

DECLARE @generated_keys table([Id] uniqueidentifier)

INSERT INTO TurboEncabulators(StatorSlots)
OUTPUT inserted.TurboEncabulatorID INTO @generated_keys
VALUES('Malleable logarithmic casing');

SELECT t.[TurboEncabulatorID ]
FROM @generated_keys AS g 
   JOIN dbo.TurboEncabulators AS t 
   ON g.Id = t.TurboEncabulatorID 
WHERE @@ROWCOUNT > 0

Các kết quả đầu ra được lưu trữ trong một biến bảng tạm thời, được nối trở lại bảng và trả về giá trị hàng ra khỏi bảng.

Lưu ý: Tôi không biết tại sao EF lại tham gia bảng phù du trở lại bàn thực (trong trường hợp nào hai người không khớp nhau).

Nhưng đó là những gì EF làm.

Kỹ thuật này ( OUTPUT) chỉ khả dụng trên SQL Server 2008 hoặc mới hơn.

Chỉnh sửa - Lý do tham gia

Lý do mà Entity Framework tham gia trở lại bảng ban đầu, thay vì chỉ sử dụng các OUTPUTgiá trị đơn giản là vì EF cũng sử dụng kỹ thuật này để lấy hàng rowversionmới được chèn.

Bạn có thể sử dụng đồng thời lạc quan trong các mô hình khuôn khổ tổ chức của bạn bằng cách sử dụng các Timestampthuộc tính: 🕗

public class TurboEncabulator
{
   public String StatorSlots)

   [Timestamp]
   public byte[] RowVersion { get; set; }
}

Khi bạn làm điều này, Entity Framework sẽ cần rowversionhàng mới được chèn:

DECLARE @generated_keys table([Id] uniqueidentifier)

INSERT INTO TurboEncabulators(StatorSlots)
OUTPUT inserted.TurboEncabulatorID INTO @generated_keys
VALUES('Malleable logarithmic casing');

SELECT t.[TurboEncabulatorID], t.[RowVersion]
FROM @generated_keys AS g 
   JOIN dbo.TurboEncabulators AS t 
   ON g.Id = t.TurboEncabulatorID 
WHERE @@ROWCOUNT > 0

Và để lấy lại điều này, Timetsampbạn không thể sử dụng một OUTPUTmệnh đề.

Đó là bởi vì nếu có một kích hoạt trên bàn, bất kỳ Timestampbạn OUTPUT sẽ sai:

  • Chèn ban đầu. Dấu thời gian: 1
  • Mệnh đề OUTPUT xuất dấu thời gian: 1
  • kích hoạt sửa đổi hàng. Dấu thời gian: 2

Dấu thời gian được trả về sẽ không bao giờ chính xác nếu bạn có một kích hoạt trên bàn. Vì vậy, bạn phải sử dụng một riêng biệt SELECT.

Và ngay cả khi bạn sẵn sàng chịu đựng sự đảo ngược không chính xác, lý do khác để thực hiện riêng SELECTlà bạn không thể ĐẦU RA một rowversionbiến trong bảng:

DECLARE @generated_keys table([Id] uniqueidentifier, [Rowversion] timestamp)

INSERT INTO TurboEncabulators(StatorSlots)
OUTPUT inserted.TurboEncabulatorID, inserted.Rowversion INTO @generated_keys
VALUES('Malleable logarithmic casing');

Lý do thứ ba để làm điều đó là cho sự đối xứng. Khi thực hiện một UPDATEtrên bảng với một kích hoạt, bạn không thể sử dụng một OUTPUTmệnh đề. Thử làm UPDATEvới một OUTPUTkhông được hỗ trợ và sẽ báo lỗi:

Cách duy nhất để làm điều đó là với một SELECTtuyên bố tiếp theo :

UPDATE TurboEncabulators
SET StatorSlots = 'Lotus-O deltoid type'
WHERE ((TurboEncabulatorID = 1) AND (RowVersion = 792))

SELECT RowVersion
FROM TurboEncabulators
WHERE @@ROWCOUNT > 0 AND TurboEncabulatorID = 1

2
tôi tưởng tượng chúng khớp với chúng để đảm bảo tính toàn vẹn (ví dụ: trong chế độ đồng thời lạc quan, trong khi bạn đang chọn từ biến bảng, ai đó có thể đã xóa các hàng trong bộ lọc). Ngoài ra, hãy yêu bạn TurboEncabulators:)
zaitsman

16

MSDN

@@ IDENTITY, SCOPE_IDENTITY và IDENT_CURRENT là các hàm tương tự ở chỗ chúng trả về giá trị cuối cùng được chèn vào cột IDENTITY của bảng.

@@ IDENTITY và SCOPE_IDENTITY sẽ trả về giá trị nhận dạng cuối cùng được tạo trong bất kỳ bảng nào trong phiên hiện tại. Tuy nhiên, SCOPE_IDENTITY chỉ trả về giá trị trong phạm vi hiện tại; @@ IDENTITY không giới hạn trong một phạm vi cụ thể.

IDENT_CURRENT không bị giới hạn bởi phạm vi và phiên; nó được giới hạn trong một bảng được chỉ định. IDENT_CURRENT trả về giá trị danh tính được tạo cho một bảng cụ thể trong bất kỳ phiên và bất kỳ phạm vi nào. Để biết thêm thông tin, xem IDENT_CURRENT.

  • IDENT_CURRENT là một hàm lấy một bảng làm đối số.
  • @@ IDENTITY có thể trả về kết quả khó hiểu khi bạn có một kích hoạt trên bàn
  • SCOPE_IDENTITY là anh hùng của bạn hầu hết thời gian.

14

@@ IDENTITY là danh tính cuối cùng được chèn bằng Kết nối SQL hiện tại. Đây là một giá trị tốt để trả về từ một thủ tục lưu trữ chèn, trong đó bạn chỉ cần nhận dạng được chèn cho bản ghi mới của mình và không quan tâm nếu nhiều hàng được thêm vào sau đó.

SCOPE_IDENTITY là danh tính cuối cùng được chèn bằng Kết nối SQL hiện tại và trong phạm vi hiện tại - nghĩa là, nếu có IDENTITY thứ hai được chèn dựa trên trình kích hoạt sau khi chèn, thì nó sẽ không được phản ánh trong SCOPE_IDENTITY, chỉ có phần chèn bạn thực hiện . Thành thật mà nói, tôi chưa bao giờ có một lý do để sử dụng này.

IDENT_CURRENT (tablename) là danh tính cuối cùng được chèn bất kể kết nối hay phạm vi. Bạn có thể sử dụng điều này nếu bạn muốn nhận giá trị IDENTITY hiện tại cho một bảng mà bạn chưa chèn bản ghi vào.


2
Bạn không bao giờ nên sử dụng @@ danh tính cho mục đích này. Nếu ai đó thêm một kích hoạt sau, bạn sẽ mất tính toàn vẹn dữ liệu. @@ nhận dạng là một thực hành cực kỳ nguy hiểm.
HLGEM

1
"giá trị cho bảng mà bạn có << không >> đã chèn bản ghi vào." Có thật không?
Abdul Saboor

13

Tôi không thể nói với các phiên bản khác của SQL Server, nhưng vào năm 2012, xuất ra trực tiếp hoạt động tốt. Bạn không cần phải bận tâm với một bảng tạm thời.

INSERT INTO MyTable
OUTPUT INSERTED.ID
VALUES (...)

Nhân tiện, kỹ thuật này cũng hoạt động khi chèn nhiều hàng.

INSERT INTO MyTable
OUTPUT INSERTED.ID
VALUES
    (...),
    (...),
    (...)

Đầu ra

ID
2
3
4

Nếu bạn muốn sử dụng nó sau này, tôi tưởng tượng bạn cần bảng tạm thời
JohnOstern

@JohnOstern Bạn có thể sử dụng bảng tạm thời nếu bạn thích, nhưng quan điểm của tôi là đó không phải là một yêu cầu OUTPUT. Nếu bạn không cần bảng tạm thời, thì truy vấn của bạn sẽ đơn giản hơn nhiều.
MarredCheese

10

LUÔN LUÔN sử dụng scope_identity (), KHÔNG BAO GIỜ cần bất cứ điều gì khác.


13
Không hoàn toàn không bao giờ nhưng 99 lần trong số 100, bạn sẽ sử dụng Scope_Identity ().
CJM

Vì cái gì bạn đã từng dùng cái gì khác?
erikkallen

11
nếu bạn chèn một số hàng bằng INSERT-SELECT, bạn sẽ cần phải chụp nhiều ID bằng mệnh đề OUTPUT
KM.

1
@KM: Có, nhưng tôi đã đề cập đến scope_identity vs @@ nhận dạng so với nhận dạng. OUTPUT là một lớp hoàn toàn khác và thường hữu ích.
erikkallen

2
Hãy xem câu trả lời của Orry ( stackoverflow.com/a/6073578/2440976 ) cho câu hỏi này - song song, và giống như một cách thực hành tốt nhất, bạn sẽ khôn ngoan khi theo dõi thiết lập của anh ấy ... thật tuyệt vời!
Dan B

2

Tạo một uuidvà cũng chèn nó vào một cột. Sau đó, bạn có thể dễ dàng xác định hàng của bạn với uuid. Đó là giải pháp làm việc 100% duy nhất bạn có thể thực hiện. Tất cả các giải pháp khác quá phức tạp hoặc không hoạt động trong cùng một trường hợp cạnh. Ví dụ:

1) Tạo hàng

INSERT INTO table (uuid, name, street, zip) 
        VALUES ('2f802845-447b-4caa-8783-2086a0a8d437', 'Peter', 'Mainstreet 7', '88888');

2) Nhận hàng đã tạo

SELECT * FROM table WHERE uuid='2f802845-447b-4caa-8783-2086a0a8d437';

Đừng quên tạo một chỉ mục cho uuidcơ sở dữ liệu. Vì vậy, hàng sẽ được tìm thấy nhanh hơn.
Frank Roth

Đối với node.js, bạn có thể sử dụng mô-đun này để tạo uuid : https://www.npmjs.com/package/uuid. const uuidv4 = require('uuid/v4'); const uuid = uuidv4()
Frank Roth

GUID không phải là một giá trị nhận dạng, nó có một số lần rút lại so với một số nguyên đơn giản.
Alejandro

1

Một cách khác để đảm bảo danh tính của các hàng bạn chèn là chỉ định các giá trị nhận dạng và sử dụng SET IDENTITY_INSERT ONvà sau đó OFF. Điều này đảm bảo bạn biết chính xác các giá trị nhận dạng là gì! Miễn là các giá trị không được sử dụng thì bạn có thể chèn các giá trị này vào cột định danh.

CREATE TABLE #foo 
  ( 
     fooid   INT IDENTITY NOT NULL, 
     fooname VARCHAR(20) 
  ) 

SELECT @@Identity            AS [@@Identity], 
       Scope_identity()      AS [SCOPE_IDENTITY()], 
       Ident_current('#Foo') AS [IDENT_CURRENT] 

SET IDENTITY_INSERT #foo ON 

INSERT INTO #foo 
            (fooid, 
             fooname) 
VALUES      (1, 
             'one'), 
            (2, 
             'Two') 

SET IDENTITY_INSERT #foo OFF 

SELECT @@Identity            AS [@@Identity], 
       Scope_identity()      AS [SCOPE_IDENTITY()], 
       Ident_current('#Foo') AS [IDENT_CURRENT] 

INSERT INTO #foo 
            (fooname) 
VALUES      ('Three') 

SELECT @@Identity            AS [@@Identity], 
       Scope_identity()      AS [SCOPE_IDENTITY()], 
       Ident_current('#Foo') AS [IDENT_CURRENT] 

-- YOU CAN INSERT  
SET IDENTITY_INSERT #foo ON 

INSERT INTO #foo 
            (fooid, 
             fooname) 
VALUES      (10, 
             'Ten'), 
            (11, 
             'Eleven') 

SET IDENTITY_INSERT #foo OFF 

SELECT @@Identity            AS [@@Identity], 
       Scope_identity()      AS [SCOPE_IDENTITY()], 
       Ident_current('#Foo') AS [IDENT_CURRENT] 

SELECT * 
FROM   #foo 

Đây có thể là một kỹ thuật rất hữu ích nếu bạn đang tải dữ liệu từ một nguồn khác hoặc hợp nhất dữ liệu từ hai cơ sở dữ liệu, v.v.


0

Mặc dù đây là một luồng cũ hơn, nhưng có một cách mới hơn để thực hiện điều này nhằm tránh một số cạm bẫy của cột IDENTITY trong các phiên bản SQL Server cũ hơn, như các lỗ hổng trong các giá trị nhận dạng sau khi máy chủ khởi động lại . Các chuỗi có sẵn trong SQL Server 2016 và chuyển tiếp, cách mới hơn là tạo một đối tượng SEQUENCE bằng TSQL. Điều này cho phép bạn tạo đối tượng chuỗi số của riêng mình trong SQL Server và kiểm soát mức tăng của nó.

Đây là một ví dụ:

CREATE SEQUENCE CountBy1  
    START WITH 1  
    INCREMENT BY 1 ;  
GO  

Sau đó, trong TSQL, bạn sẽ làm như sau để có được ID chuỗi tiếp theo:

SELECT NEXT VALUE FOR CountBy1 AS SequenceID
GO

Dưới đây là các liên kết để TẠO SEQUENCEGIÁ TRỊ TIẾP THEO


Các chuỗi có cùng các vấn đề về bản sắc, như các khoảng trống (không thực sự có vấn đề).
Alejandro

-1

Sau câu lệnh chèn của bạn, bạn cần thêm nó. Và hãy chắc chắn về tên bảng nơi dữ liệu được chèn. Bạn sẽ nhận được hàng hiện tại không có hàng nào bị ảnh hưởng bởi câu lệnh chèn của bạn.

IDENT_CURRENT('tableName')

2
Bạn có nhận thấy chính xác đề nghị này đã được trả lời nhiều lần trước đây không?
TT.

Đúng. nhưng tôi đang cố gắng mô tả giải pháp theo cách riêng của tôi.
Khan Ataur Rahman

Và nếu người khác đã chèn một hàng vào giữa câu lệnh chèn của bạn và cuộc gọi IDENT_CURRENT () của bạn, bạn sẽ nhận được id của bản ghi mà người khác đã chèn - có thể không phải là điều bạn muốn. Như đã lưu ý trong hầu hết các câu trả lời ở trên - trong hầu hết các trường hợp, bạn nên sử dụng SCOPE_IDENTITY ().
Trondster
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.