2016-08-19 14 views
8

Về cơ bản, tôi đang cố gắng hiểu cách viết mã giao dịch đúng (hoặc "viết chính xác") khi phát triển dịch vụ REST với Jax-RS và Spring . Ngoài ra, chúng tôi đang sử dụng JOOQ để truy cập dữ liệu. Nhưng điều đó không liên quan lắm ...
Hãy xem xét mô hình đơn giản, nơi chúng tôi có một số tổ chức, có các trường sau: "id", "name", "code". Tất cả đều phải là duy nhất. Ngoài ra, có trường status.
Tổ chức có thể bị xóa tại một số thời điểm. Nhưng chúng tôi không muốn xóa hoàn toàn dữ liệu vì chúng tôi muốn lưu dữ liệu cho mục đích phân tích/bảo trì. Vì vậy, chúng tôi chỉ đặt trường 'trạng thái' của tổ chức thành 'REMOVED'.
Vì chúng tôi không xóa hàng tổ chức khỏi bảng, chúng tôi không thể đặt ràng buộc duy nhất vào cột "tên", bởi vì, chúng tôi có thể xóa tổ chức và sau đó tạo một tổ chức mới có cùng tên. Nhưng hãy giả sử rằng mã phải là duy nhất trên toàn cầu, vì vậy chúng tôi có một ràng buộc duy nhất trên cột code.Cách viết mã giao dịch chính xác/đáng tin cậy với JAX-RS và Spring

Vì vậy, với điều đó, chúng ta hãy xem ví dụ đơn giản này, tạo ra tổ chức, thực hiện một số kiểm tra trên đường đi.

Resource:

@Component 
@Path("/api/organizations/{organizationId: [0-9]+}") 
@Consumes(MediaType.APPLICATION_JSON) 
@Produces(MediaTypeEx.APPLICATION_JSON_UTF_8) 
public class OrganizationResource { 
    @Autowired 
    private OrganizationService organizationService; 

    @Autowired 
    private DtoConverter dtoConverter; 

    @POST 
    public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) { 

     if (organizationService.checkOrganizationWithNameExists(request.name())) { 
      // this throws special Exception which is intercepted and translated to response with 409 status code 
      throw Responses.abortConflict("organization.nameExist", ImmutableMap.of("name", request.name())); 
     } 

     if (organizationService.checkOrganizationWithCodeExists(request.code())) { 
      throw Responses.abortConflict("organization.codeExists", ImmutableMap.of("code", request.code())); 
     } 

     long organizationId = organizationService.create(person.user().id(), request.name(), request.code()); 
     return dtoConverter.from(organization.findById(organizationId)); 
    } 
} 

dịch vụ DAO trông như thế:

@Transactional(DBConstants.SOME_TRANSACTION_MANAGER) 
public class OrganizationServiceImpl implements OrganizationService { 
    @Autowired 
    @Qualifier(DBConstants.SOME_DSL) 
    protected DSLContext context; 

    @Override 
    public long create(long userId, String name, String code) { 
     Organization organization = new Organization(null, userId, name, code, OrganizationStatus.ACTIVE); 
     OrganizationRecord organizationRecord = JooqUtil.insert(context, organization, ORGANIZATION); 
     return organizationRecord.getId(); 
    } 

    @Override 
    public boolean checkOrganizationWithNameExists(String name) { 
     return checkOrganizationExists(Tables.ORGANIZATION.NAME, name); 
    } 

    @Override 
    public boolean checkOrganizationWithCodeExists(String code) { 
     return checkOrganizationExists(Tables.ORGANIZATION.CODE, code); 
    } 

    private boolean checkOrganizationExists(TableField<OrganizationRecord, String> checkField, String checkValue) { 
     return context.selectCount() 
       .from(Tables.ORGANIZATION) 
       .where(checkField.eq(checkValue)) 
       .and(Tables.ORGANIZATION.ORGANIZATION_STATUS.ne(OrganizationStatus.REMOVED)) 
       .fetchOne(DSL.count()) > 0; 
    } 
} 

này mang lại một số câu hỏi:

  1. Tôi có nên đặt @Transactional chú thích về phương pháp createOrganization Resource không? Hoặc tôi có nên tạo thêm một dịch vụ nói chuyện với DAO và đặt chú thích @Transactional vào phương thức của nó không? Thứ gì khác?
  2. Điều gì sẽ xảy ra nếu hai người dùng đồng thời gửi yêu cầu với cùng một trường "code". Trước khi giao dịch đầu tiên được cam kết kiểm tra được thông qua thành công, vì vậy không có 409 respones sẽ được gửi đi. Hơn giao dịch đầu tiên sẽ được cam kết đúng, nhưng giao dịch thứ hai sẽ vi phạm ràng buộc DB. Điều này sẽ ném SQLException. Làm thế nào để xử lý một cách duyên dáng điều đó? Tôi có nghĩa là tôi vẫn muốn hiển thị thông báo lỗi tốt đẹp ở phía khách hàng, nói rằng tên đó đã được sử dụng. Nhưng tôi không thể phân tích SQLException hay smth .. tôi có thể không?
  3. Tương tự như trang trước, nhưng lần này "tên" không phải là duy nhất. Trong trường hợp này, giao dịch thứ hai sẽ không vi phạm bất kỳ ràng buộc nào, dẫn đến việc có hai tổ chức có cùng tên, vi phạm ràng buộc kinh doanh của chúng tôi.
  4. Tôi có thể xem/tìm hiểu hướng dẫn/mã/v.v. ở đâu, bạn xem xét các ví dụ tuyệt vời về cách viết mã REST + DB chính xác/đáng tin cậy với logic kinh doanh phức tạp. Github/sách/blog, bất cứ điều gì. Tôi đã cố gắng để tìm một cái gì đó như myselft, nhưng hầu hết các ví dụ chỉ tập trung vào hệ thống ống nước - thêm các libs để quạ, sử dụng các chú thích, có CRUD đơn giản của bạn, kết thúc. Chúng không chứa bất kỳ sự xem xét giao dịch nào cả. I E.

CẬP NHẬT: Tôi biết về mức cách ly và thông thường error/isolation matrix (đọc bẩn, v.v.). Vấn đề tôi gặp phải là tìm một số mẫu "sẵn sàng sản xuất" để học hỏi. Hoặc một cuốn sách hay về một chủ đề. Tôi vẫn không thực sự làm thế nào để xử lý tất cả các lỗi đúng .. Tôi đoán tôi cần phải thử lại một vài lần, nếu giao dịch thất bại .. và hơn là chỉ ném một số lỗi chung và thực hiện khách hàng, mà xử lý rằng .. Nhưng làm Tôi thực sự phải sử dụng chế độ SERIALIZABLE, bất cứ khi nào tôi sử dụng các truy vấn phạm vi? Bởi vì nó sẽ ảnh hưởng đến hiệu suất rất nhiều. Nhưng nếu không thì làm thế nào tôi có thể garantee rằng giao dịch sẽ thất bại ..

Dù sao tôi đã quyết định rằng bây giờ tôi cần thêm thời gian để tìm hiểu về giao dịch và quản lý db nói chung để giải quyết vấn đề này ...

Trả lời

-1

Trước hết lớp DAO nên thậm chí không biết nó đang được fronted bởi một REST webservice. Hãy chắc chắn phân chia trách nhiệm.

Giữ @Transactional trên DAO. Nếu bạn đang phát hành chỉ một tuyên bố duy nhất hơn bạn cần phải quyết định xem bạn có OK với đọc bẩn. Về cơ bản, tìm ra mức độ cách ly thấp nhất là gì cho ứng dụng của bạn. Mỗi phương thức sẽ bắt đầu một Giao dịch mới (trừ khi được gọi từ một phương thức khác đã có một khởi động) và nếu có bất kỳ ngoại lệ nào được ném, nó sẽ quay trở lại bất kỳ cuộc gọi nào. Bạn có thể thiết lập một ExceptionHandler tùy chỉnh trong Controller của bạn để xử lý các SQLDataIntegrityExceptions (như ví dụ chèn "mã").

Sử dụng một tổng hợp Primary Key rằng bìa (id, tên, mã, tình trạng), do đó bạn có thể có một org có cùng tên nhưng ai sẽ là "CURRENT" và ai sẽ được "REMOVED"

+0

Vâng, những gì nếu có những trạng thái có giá trị khác bị đình chỉ, vv? Vấn đề là tôi không thể làm điều đó với các ràng buộc đơn giản ... Tôi có nên viết các trigger phức tạp cho mọi ràng buộc phức tạp, do đó sao chép những gì tôi đã viết trong Java? Ngoài ra làm thế nào để phục hồi một cách duyên dáng từ vi phạm những hạn chế đó? Và tôi có thể tìm thấy ví dụ điển hình về mã theo các thực tiễn đó ở đâu? –

+0

phạm vi giao dịch là đơn vị công việc, đơn vị công trình không được xác định ở cấp DAO, nói chung có chú thích @Transactionnal ở mức DAO là mùi thiết kế – Gab

+0

@Gab Rất không đồng ý - bạn nghĩ thông tin giao dịch ở đâu được xác định nếu không gần nơi tương tác của cơ sở dữ liệu? – Gandalf

1

Nói chung, mà không nói về giao dịch, điểm cuối chỉ nên lấy tham số từ yêu cầu và gọi Dịch vụ. Nó không nên làm logic kinh doanh.

Dường như phương thức kiểm traXXX của bạn là một phần của logic nghiệp vụ, bởi vì chúng ném lỗi về xung đột tên miền cụ thể. Tại sao không đặt chúng vào Dịch vụ thành một phương pháp, đó là bằng cách giao dịch?

//service code 
public Organization createOrganization(String userId, String name, String code) { 

    if (this.checkOrganizationWithNameExists(request.name())) { 
     throw ... 
    } 

    if (this.checkOrganizationWithCodeExists(code)) { 
     throw ... 
    } 

    long organizationId = this.create(userId, name, code); 
    return dao.findById(organizationId); 
} 

Tôi lấy làm tham số của bạn là Chuỗi, nhưng chúng có thể là bất kỳ thứ gì. Tôi không chắc bạn muốn đưa ra Responses.abortConflict trong lớp dịch vụ vì nó có vẻ là một khái niệm REST, nhưng bạn có thể định nghĩa các kiểu ngoại lệ của riêng mình cho nó nếu bạn muốn.

Endpoint mã nên trông như thế này, tuy nhiên, nó có thể chứa thêm khối try-catch mà chuyển đổi các trường hợp ngoại lệ ném vào Lỗi phản ứng:

//endpoint code 
@POST 
public OrganizationResponse createOrganization(@Auth Person person, CreateOrganizationRequest request) { 
    String code = request.code(); 
    String name = request.name(); 
    String userId = person.user().id(); 
    return dtoConverter.from(organizationService.createOrganization(userId, name, code)); 
} 

Đối với câu hỏi 2 và 3, transaction isolation levels là bạn bè. Đặt mức cô lập đủ cao. Tôi nghĩ rằng 'đọc lặp lại' là một trong những phù hợp trong trường hợp của bạn. Các phương thức kiểm traXXX của bạn sẽ phát hiện nếu một số giao dịch khác cam kết các thực thể có cùng tên hoặc mã và nó được đảm bảo rằng các tình huống được giữ nguyên theo phương thức 'create' được thực thi. Một thêm useful read liên quan đến mức độ cách ly mùa xuân và giao dịch.

+0

Nhưng nó vẫn không giúp giải quyết vấn đề với dữ liệu không chính xác. Có nghĩa là câu hỏi 2 và 3 trong câu hỏi ... –

+0

Kiểm tra dữ liệu được thực hiện trong dịch vụ và bạn ném ngoại lệ của riêng bạn. Bạn không phải phân tích SQLException. Việc cô lập giao dịch đảm bảo rằng khi bạn đã thực hiện các kiểm tra trong dịch vụ, chúng vẫn hợp lệ trong câu lệnh 'create'. Ngay cả khi hai yêu cầu đến cùng một lúc, một trong số chúng sẽ bị trì hoãn cho đến khi yêu cầu khác được thực hiện (hoặc một cái gì đó tương tự, nó phụ thuộc vào cấp độ). Tôi nghĩ rằng điều này bao gồm câu hỏi 2 và 3. – pcjuzer

+0

Nó không phải là đơn giản ... cô lập SERIALIZABLE là quá hạn chế để sử dụng cho mọi trường hợp, nhưng ngay cả REPEATABLE_READ là không đủ. Tôi đã phải sử dụng SELECT FOR UPDATE để làm cho nó hoạt động bằng cách nào đó, nhưng theo cách đó đồng thời sẽ bị ... BTW, tôi đã đưa dự án thử nghiệm trên github, vì vậy bất cứ ai có thể chơi với nó ... https://github.com/ Fantast/gs-quản lý-giao dịch –

1

Theo hiểu biết của tôi, cách tốt nhất để xử lý giao dịch cấp DB, bạn phải sử dụng tính năng phân tách cách ly của Spring theo cách hiệu quả trong lớp dao. Dưới đây là ngành công nghiệp mẫu chuẩn codde trong trường hợp của bạn ...

public interface OrganizationService { 
    @Retryable(maxAttempts=3,value=DataAccessResourceFailureException.class,[email protected](delay = 1000)) 
    public boolean checkOrganizationWithNameExists(String name);  
} 

@Repository 
@EnableRetry 
public class OrganizationServiceImpl implements OrganizationService { 
    @Transactional(isolation = Isolation.READ_COMMITTED) 
    @Override 
    public boolean checkOrganizationWithNameExists(String name){ 
     //your code 
     return true;  
    } 
} 

hãy véo cho tôi nếu tôi sai ở đây

0

Tách quan tâm:

  • nguồn Jax-rs (endpoint) lớp: chỉ cần xử lý yêu cầu, gọi dịch vụ và bọc ngoại lệ tiềm năng trong mã phản hồi thích hợp (chỉ cần nắm bắt và quấn thủ công hoặc sử dụng exception mapper).
  • Lớp dịch vụ/kinh doanh: hiển thị phương thức giao dịch cho mỗi đơn vị công việc, lỗi kinh doanh phải được xử lý như ngoại lệ đã chọn, các thao tác không được chọn (lớp con của RuntimeException).
  • Lớp truy cập dữ liệu: chỉ cần xử lý nội dung truy cập dữ liệu (tức lànhận được bối cảnh db, thực thi truy vấn và cuối cùng ánh xạ kết quả).

Tôi nhấn mạnh vào một điều, nơi tốt để có ranh giới giao dịch là nơi mà phương thức kinh doanh của bạn được xác định. Phạm vi giao dịch phải là đơn vị kinh doanh của công việc.

Về vấn đề tương tranh, có 2 cách để xử lý loại sự cố đồng thời này: khóa bi quan hoặc lạc quan.

  • Bi quan:

    • Khóa
    • làm công cụ của bạn
    • Cập nhật
    • khóa phát hành
  • lạc:

    • phiên bản kiểm tra
    • làm công cụ của bạn
    • cập nhật nếu phiên bản là giống nhau, không khác

Bi quan là một vấn đề liên quan đến khả năng mở rộng và hiệu suất, vấn đề lạc quan là bạn đôi khi kết thúc bằng cách gửi một lỗi hoạt động cho người dùng cuối.

Cá nhân tôi sẽ đi với khóa lạc quan trong trường hợp của bạn, JOOQ support it

+0

Để khóa lạc quan, Không rõ tôi nên phiên bản các bản ghi nào? Tôi muốn bảo vệ hoạt động INSERT khỏi việc tạo các mục trùng lặp. Nhưng trong trường hợp đó tôi không thực sự thay đổi bất kỳ hồ sơ nào, phải không? Tôi đoán, tôi có thể làm cho nó hoạt động bằng cách nào đó .. như tạo bảng chứa tất cả các tên đã từng sử dụng - nhưng có vẻ xấu xí ... Đối với khóa bi quan - bạn có nghĩa là tôi nên sử dụng 'SELECT..FOR UPDATE'? Hoặc chỉ sử dụng cách ly 'REPEATABLE READ'? –

+0

Tôi không thể hiểu, nếu nó thực sự bảo vệ trường hợp của tôi, nơi tôi INSERT hồ sơ mới như là một quyết định được thực hiện bằng cách chọn các hồ sơ khác, I.e. kiểm tra nếu tên tồn tại, sau đó chèn bản ghi mới với tên này, nếu không thì không làm gì cả. –

+0

Sry Tôi đọc quá nhanh. 'REPEATABLE READ' không ngăn chặn chèn dữ liệu mới, bạn phải khóa toàn bộ bảng và do đó đặt mức thành serializable. Tôi cho rằng 'chọn để cập nhật' không chỉ khóa câu lệnh được chọn và vì vậy không phù hợp. Bạn có thể duy trì một mục nhập phiên bản bảng org trong một bảng chuyên dụng để thực hiện khóa lạc quan và chèn phiên bản org và tăng mới trong cùng một giao dịch nhưng nó không phải là rất thanh lịch – Gab

Các vấn đề liên quan