2012-04-25 36 views
28

Ứng dụng của tôi đang sử dụng phiên scoped và kiểu khai báo của SQLALchemy. Đó là một ứng dụng web và rất nhiều lần chèn DB được thực thi bởi Celery, một công cụ lên lịch tác vụ.Xử lý các khóa chính trùng lặp khi chèn trong SQLAlchemy (kiểu khai báo)

Thông thường, khi quyết định để chèn một đối tượng, mã của tôi có thể làm điều gì đó dọc theo dòng sau đây:

from schema import Session 
from schema.models import Bike 

pk = 123 # primary key 
bike = Session.query(Bike).filter_by(bike_id=pk).first() 
if not bike: # no bike in DB 
    new_bike = Bike(pk, "shiny", "bike") 
    Session.add(new_bike) 
    Session.commit() 

Vấn đề ở đây là vì có rất nhiều này được thực hiện bởi người lao động không đồng bộ, nó có thể cho một làm việc ở nửa chừng mặc dù chèn Bike với id=123, trong khi một số khác đang kiểm tra sự tồn tại của nó. Trong trường hợp này, nhân viên thứ hai sẽ thử và chèn một hàng có cùng khóa chính và SQLAlchemy sẽ tăng IntegrityError.

Tôi không thể cho cuộc sống của tôi tìm thấy một cách tốt đẹp để đối phó với vấn đề này ngoài việc trao đổi ra Session.commit() cho:

'''schema/__init__.py''' 
from sqlalchemy.orm import scoped_session, sessionmaker 
Session = scoped_session(sessionmaker()) 

def commit(ignore=False): 
    try: 
     Session.commit() 
    except IntegrityError as e: 
     reason = e.message 
     logger.warning(reason) 

     if not ignore: 
      raise e 

     if "Duplicate entry" in reason: 
      logger.info("%s already in table." % e.params[0]) 
      Session.rollback() 

Và sau đó ở khắp mọi nơi tôi có Session.commit bây giờ tôi có schema.commit(ignore=True) nơi tôi don' Hãy nhớ rằng hàng không được chèn lại.

Đối với tôi, điều này có vẻ rất dễ vỡ do kiểm tra chuỗi. Cũng như một FYI, khi một IntegrityError được nâng lên nó trông như thế này:

(IntegrityError) (1062, "Duplicate entry '123' for key 'PRIMARY'") 

Vì vậy, tất nhiên là chìa khóa chính tôi đã chèn được cái gì đó như Duplicate entry is a cool thing sau đó tôi cho rằng tôi có thể bỏ lỡ IntegrityError 's mà không thực sự vì các khóa chính trùng lặp.

Có bất kỳ phương pháp tốt hơn, mà duy trì cách tiếp cận SQLAlchemy sạch Tôi đang sử dụng (như trái ngược với bắt đầu để viết ra báo cáo trong chuỗi vv..)

Db là MySQL (mặc dù cho kiểm tra đơn vị tôi thích để sử dụng SQLite và không muốn cản trở khả năng đó với bất kỳ phương pháp tiếp cận mới nào).

Chúc mừng!

+3

tại sao bạn không xem xét sử dụng tự động tăng để tạo ra bạn khóa chính? thì bạn không phải lo lắng về vấn đề này. Hoặc có lý do cụ thể không để làm điều đó? – mata

+0

Có một lý do cụ thể (xin lỗi, ví dụ là một chút tầm thường). – Edwardr

Trả lời

6

Bạn nên xử lý mọi cách IntegrityError theo cùng một cách: quay lại giao dịch và thử lại tùy chọn. Một số cơ sở dữ liệu thậm chí sẽ không cho phép bạn làm gì hơn sau IntegrityError. Bạn cũng có thể có được một khóa trên bàn, hoặc một khóa hạt mịn hơn nếu cơ sở dữ liệu cho phép nó, ở đầu của hai giao dịch xung đột.

Sử dụng câu lệnh with để bắt đầu một cách rõ ràng một giao dịch, và tự động cam kết (hoặc rollback trên bất kỳ ngoại lệ):

from schema import Session 
from schema.models import Bike 

session = Session() 
with session.begin(): 
    pk = 123 # primary key 
    bike = session.query(Bike).filter_by(bike_id=pk).first() 
    if not bike: # no bike in DB 
     new_bike = Bike(pk, "shiny", "bike") 
     session.add(new_bike) 
+0

Xin chào. Tôi không lên kế hoạch chèn và kiểm tra cùng một lúc với mục đích. Vấn đề là đối tượng xảy ra sẽ được tạo ra bởi hai quy trình riêng biệt theo cách đặc biệt. Không có gì là không quan tâm về điều đó, nó chỉ là cách ứng dụng (trong thực tế các đối tượng không phải là xe đạp, họ đang * lần *). Tuy nhiên, bạn nói đúng về việc chạy một công nhân. Tôi tìm hiểu cách chỉ định một nhân viên quản lý tất cả các nhiệm vụ liên quan đến DB, điều này sẽ cung cấp sự đồng bộ mà tôi yêu cầu. Thực hiện chèn từ ứng dụng không phải là một tùy chọn. DB nằm trên một máy từ xa và tôi cần các phản hồi phụ ứng dụng web 100ms. – Edwardr

+0

Thiết kế hầu như luôn đổ lỗi cho các loại vấn đề SQL này. Ví dụ: bạn có chắc là bạn không thể làm cho tự động tăng khóa chính của cơ sở dữ liệu và đối phó với kết quả 'hai hàng' trước đây của cột khóa chính trước đó không? – joeforker

+0

[Xin lỗi, tôi nên thêm, có lý do chính đáng khiến PK không tự động tăng] Tôi không chắc mình đồng ý. DB được chia sẻ bởi nhiều ứng dụng khác, bao gồm cả việc sử dụng bảng được đề cập. Tại sao thiết kế tồi tệ là một DB có thể có một hàng chèn một quá trình/ứng dụng/con người khác của tôi sau khi bạn đã thực hiện một số thẩm định để kiểm tra? Vấn đề là bạn phải đối phó với điều đó trong ứng dụng của bạn. Câu hỏi của tôi chỉ đơn giản là cách duy nhất tôi có thể thấy để đối phó với điều đó trong SQLAlchemy là thông qua kiểm tra chuỗi, và nó không có vẻ đặc biệt mạnh mẽ. – Edwardr

3

Tôi giả định rằng từ khóa chính của bạn ở đây là tự nhiên một cách nào đó, đó là lý do bạn không thể dựa vào kỹ thuật autoincrement bình thường. Vì vậy, chúng ta hãy nói vấn đề thực sự là một trong một số cột duy nhất mà bạn cần chèn, đó là phổ biến hơn.

nếu bạn muốn "cố gắng chèn, khôi phục một phần về lỗi", bạn sử dụng SAVEPOINT, với SQLAlchemy là begin_nested(). rollback tiếp theo() hoặc commit() chỉ hoạt động dựa trên SAVEPOINT đó, không phải là khoảng thời gian lớn hơn.

Tuy nhiên, tổng thể mẫu ở đây chỉ là một mẫu thực sự nên tránh. Những gì bạn thực sự muốn làm ở đây là một trong ba điều. 1.Không chạy các công việc đồng thời xử lý cùng các khóa cần được chèn vào. 2. đồng bộ hóa các công việc bằng cách nào đó trên các phím đồng thời đang được làm việc và 3. sử dụng một số dịch vụ chung để tạo bản ghi mới của loại đặc biệt này, được chia sẻ bởi các công việc (hoặc đảm bảo rằng tất cả chúng được thiết lập trước khi công việc chạy).

Nếu bạn nghĩ về nó, # 2 diễn ra trong mọi trường hợp với mức độ cô lập cao. Bắt đầu hai phiên postgres. Phần 1:

test=> create table foo(id integer primary key); 
NOTICE: CREATE TABLE/PRIMARY KEY will create implicit index "foo_pkey" for table "foo" 
CREATE TABLE 
test=> begin; 
BEGIN 
test=> insert into foo (id) values (1); 

phiên 2:

test=> begin; 
BEGIN 
test=> insert into foo(id) values(1); 

những gì bạn sẽ thấy là, phiên 2 khối, như hàng với PK # 1 bị khóa. Tôi không chắc liệu MySQL có đủ thông minh để làm điều này không, nhưng đó là hành vi đúng. Nếu OTOH bạn cố gắng chèn một PK khác:

^CCancel request sent 
ERROR: canceling statement due to user request 
test=> rollback; 
ROLLBACK 
test=> begin; 
BEGIN 
test=> insert into foo(id) values(2); 
INSERT 0 1 
test=> \q 

nó chỉ hoạt động tốt mà không bị chặn.

Vấn đề là nếu bạn đang thực hiện kiểu tranh luận PK/UQ này, nhiệm vụ cần tây của bạn sẽ tự sắp xếp theo thứ tự dù sao hoặc ít nhất, chúng phải như vậy.

23

Nếu bạn sử dụng session.merge(bike) thay vì session.add(bike), thì bạn sẽ không tạo ra lỗi khóa chính. Các bike sẽ được lấy ra và cập nhật hoặc tạo ra khi cần thiết.

+6

Nếu bạn sử dụng hợp nhất bạn vẫn có thể nhận được lỗi toàn vẹn, nếu bạn thực hiện hai lần hợp nhất trên các phiên khác nhau cùng một lúc. – Sjoerd

+0

Câu trả lời này là tốt khi phiên phù hợp trong bộ nhớ, nhưng không tốt cho các truy vấn lớn hơn. Vì vậy, nếu bạn muốn thêm nhiều dữ liệu hơn so với phù hợp trong bộ nhớ, bạn không thể chỉ tạo ra một loạt các phiên và hợp nhất chúng, phải không? – elplatt

2

Thay vì session.add(obj) bạn cần sử dụng các mã được đề cập dưới đây, điều này sẽ sạch hơn nhiều và bạn sẽ không cần sử dụng chức năng cam kết tùy chỉnh như bạn đã đề cập. Điều này sẽ bỏ qua xung đột, tuy nhiên, không chỉ cho khóa trùng lặp mà còn cho cả những người khác nữa.

mysql:

self.session.execute(insert(self.table, values=values, prefixes=['IGNORE'])) 

sqlite

self.session.execute(insert(self.table, values=values, prefixes=['OR IGNORE'])) 
Các vấn đề liên quan