简体   繁体   English

在 Entity Framework 和 Sql Server 中创建对象计数器

[英]Creating object counters in Entity Framework and Sql Server

Note 1: I REPHRASED THE QUESTION.注 1:我改写了这个问题。 It now consists of Suppliers and Orders, instead of Cars and Parts.它现在由供应商和订单组成,而不是汽车和零件。

Note 2: THIS PROBLEM IS HYPOTHETICAL.注 2:此问题是假设性的。 My goal is to understand how to create object counters.我的目标是了解如何创建对象计数器。

For regulatory requirements, I need TO SEQUENTIALLY NUMBER EACH Order for each of the suppliers.对于监管要求,我需要对每个供应商的每个订单进行顺序编号。

I'm Using 'Entity Framework` with Sql Server.我在 Sql Server 中使用“实体框架”

In my hypothetical example, I have a Supplier class and an Order class.在我的假设示例中,我有一个Supplier类和一个Order类。

Each supplier has Orders .每个供应商都有Orders Each order has a product and a quantity.每个订单都有一个产品和一个数量。 Meaning, it states which product was ordered from the supplier and how many of it.意思是,它说明从供应商处订购了哪些产品以及订购了多少。

I need to be able to create counters, like an auto incremented number, to count the orders FOR EACH supplier .我需要能够创建计数器,比如一个自动递增的数字,来计算每个供应商的订单。

For regulatory reasons, each supplier must sequentially number its orders, in the order of creation, and using an integer only.出于监管原因,每个供应商必须按创建顺序对其订单进行顺序编号,并且仅使用整数。

When we examine an Order , We should know by its OrderCountForSupplier column, what was its order of creation (a DateTime / TimeStamp column is insufficient by the regulatory authorities. They require such a counter).当我们检查一个Order ,我们应该通过它的OrderCountForSupplier列知道它的创建顺序是什么(监管机构的DateTime / TimeStamp列是不够的。他们需要这样的计数器)。

For simplicity of this question, an order cannot be deleted (it's status can change, but it cannot be deleted).为简单起见,无法删除订单(其状态可以更改,但无法删除)。

It's very important for me to have a solution which includes the technical/programming way, not only theoretic way.对我来说,拥有一个包括技术/编程方式,而不仅仅是理论方式的解决方案非常重要。

I've made a diagram in order to explain my problem in the most clear way possible:我制作了一个图表,以便以最清晰的方式解释我的问题:

在此处输入图片说明


I have a way that might work, and would be glad to hear feedback.我有一种可能有效的方法,很高兴听到反馈。

I'm thinking of an external table/tables, to hold the counters.我正在考虑使用外部桌子/桌子来放置计数器。 Something like:就像是:

Supplier Order Counters Table

| SupplierId | OrderCountForSupplier
------------------------
| 54654         | 3
| 78787         | 2
| 99666         | 4

Would I need a trigger in order to increment the OrderCountForSupplier counter on each insertion, for each supplier?对于每个供应商,我是否需要触发器才能在每次插入时增加OrderCountForSupplier计数器?

If not - how can this incremental be done in a safe way ?如果没有 - 如何以安全的方式完成这个增量? (without for example, two processes in a race condition to get the next counter and increment it, which could eventually result in a duplicate Order Count). (例如,没有两个进程在竞争条件下获取下一个计数器并增加它,这最终可能导致重复的订单计数)。

And another note: Can this be done Entity Framework wise?另一个注意事项:这可以通过Entity Framework明智地完成吗? if not - a Sql Server solution will be respected.如果不是 - 将Sql Server解决方案。

First answer , the example in the question has changed after it was written.第一个回答,问题中的例子在写完之后发生了变化。

You say that is it OK to have gaps in the Part IDs, because "some parts might be deleted along the way".您说部件 ID 中存在间隙是可以的,因为“某些部件可能会在此过程中被删除”。

So, what's the difference between your example:那么,您的示例之间有什么区别:

Car      PartID
54654    1
54654    2
54654    3
78787    1
78787    2
99666    1
99666    2
99666    5
99666    7

And this variant:这个变种:

Car      PartID
54654    1
54654    2
54654    3
78787    4
78787    5
99666    6
99666    7
99666    8
99666    9

In the second variant each part has some ID that is unique for each car (it is also globally unique as well, but it doesn't matter).在第二个变体中,每个部件都有一些对每辆车来说都是唯一的 ID(它也是全局唯一的,但这并不重要)。 In the second variant PartID specifies the order in which parts were inserted into the table, same as in the first variant.在第二个变体中PartID指定了零件插入表中的顺序,与第一个变体相同。

So, I'd use a simple IDENTITY column:所以,我会使用一个简单的IDENTITY列:

Parts部分

PartID int IDENTITY NOT NULL (PRIMARY KEY)
CarLicenseNum int NOT NULL (FOREIGN KEY)
PartName varchar(255)

Update for Supplier-Order example供应商订单示例的更新

The most important bit in the updated question is "regulatory reasons" .更新问题中最重要的一点是“监管原因” It answers the question why would you want to do such unnatural thing.它回答了你为什么要做这种不自然的事情的问题。 "Regulatory" and efficiency are often opposite. “监管”和效率往往是对立的。

Essentially, it means that you have to use serializable transaction isolation level when inserting a new row and calculating the next number in the sequence.本质上,这意味着在插入新行并计算序列中的下一个数字时,您必须使用可序列化事务隔离级别 It will hurt concurrency/throughput, but it will guarantee consistency and "be safe" in multi-user environment.它会损害并发/吞吐量,但它会在多用户环境中保证一致性和“安全”。

I don't know how to do it in Entity Framework, it should be possible.我不知道如何在实体框架中做到这一点,应该是可能的。 But, again, for "regulatory reasons" I'd put this logic in the stored procedure in the DB and make sure that ordinary users don't have write access to the Orders table directly, but have rights only to execute this dedicated stored procedure.但是,同样,出于“监管原因”,我将此逻辑放在数据库中的存储过程中,并确保普通用户没有直接对Orders表的写访问权限,而只有执行此专用存储过程的权限. You can replicate the logic of this stored procedure in the EF code, but the database itself will be open to changes done through other applications, which may not follow the regulatory requirements.您可以在 EF 代码中复制此存储过程的逻辑,但数据库本身将对通过其他应用程序完成的更改开放,这可能不符合法规要求。

You can implement it using the separate table, which stores the latest sequence number for each supplier, or you can read the last maximum sequence number on the fly.您可以使用单独的表来实现它,该表存储每个供应商的最新序列号,或者您可以即时读取最后一个最大序列号。 If each supplier has only few orders, then this separate table with latest values of counters would be comparable to Orders table and you would not gain much.如果每个供应商只有很少的订单,那么这个包含最新计数器值的单独表将与Orders表相当,您不会获得太多收益。 In any case, having a proper index is the key.无论如何,拥有合适的索引是关键。 Getting the latest counter value would be one seek in the index.获取最新的计数器值将是索引中的一次查找。

Here is an example of stored procedure without using an extra table.这是一个不使用额外表的存储过程示例。

Make sure that Orders table has unique index on (SupplierId, OrderCountForSupplier) .确保Orders表在(SupplierId, OrderCountForSupplier)上具有唯一索引。 In fact, you must have this index even if you are using an extra table to enforce the constraint.事实上,即使您使用额外的表来强制执行约束,您也必须拥有此索引。

CREATE PROCEDURE [dbo].[AddOrder]
    @ParamSupplierID int, 
    @ParamProductSerial varchar(10),
    @ParamQuantity int,
    @NewOrderID int OUTPUT
AS
BEGIN
    SET NOCOUNT ON; 
    SET XACT_ABORT ON;

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    BEGIN TRANSACTION;
    BEGIN TRY

        DECLARE @VarMaxCounter int;

        SELECT TOP(1) @VarMaxCounter = OrderCountForSupplier
        FROM dbo.Orders
        WHERE SupplierID = @ParamSupplierID
        ORDER BY OrderCountForSupplier DESC;

        SET @VarMaxCounter = ISNULL(@VarMaxCounter, 0) + 1;

        INSERT INTO dbo.Orders
            (SupplierID
            ,OrderCountForSupplier
            ,ProductSerial
            ,Quantity)
        VALUES
            (@ParamSupplierID
            ,@VarMaxCounter
            ,@ParamProductSerial
            ,@ParamQuantity);

        SET @NewOrderID = SCOPE_IDENTITY();

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        -- TODO: handle the error
        SET @NewOrderID = 0;
        ROLLBACK TRANSACTION;
    END CATCH;

END
GO

First things first: it is not OK to change your question entirely!第一件事:完全改变你的问题是不行的! Delete this question and create a new one.删除此问题并创建一个新问题。 Having said that ...话说回来 ...

Answer of the current question:当前问题的答案:

Answers to hypothetical questions are just oppinion based and/or too broad (there is actually a flag for this - Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. )!假设性问题的答案只是基于意见和/或过于宽泛(实际上对此有一个标志 - Many good questions generate some degree of opinion based on expert experience, but answers to this question will tend to be almost entirely based on opinions, rather than facts, references, or specific expertise. )!

My answer to the current question is: I do not see any benefit (or advantage or use) of the OrderCountForSupplier in the database !我对当前问题的回答是:我在数据库OrderCountForSupplier任何好处(或优势或用途)! Creating such counter in the database makes adding and maintenance (in a multi-threaded environment) very complicated and error-prone.在数据库中创建这样的计数器会使添加维护(在多线程环境中)变得非常复杂且容易出错。

I think the problem can be solved more easily with the help of EF (move the creation of the counters in the code) and a different design of the database:我认为借助 EF(移动代码中计数器的创建)和不同的数据库设计,可以更轻松地解决问题:

  • in order to allow concurrent adding of Orders, create two columns - a GUID as the Order -PrimaryKey and a CreationDate of type DateTime .为了允许同时添加订单,创建两列 - 一个GUID作为Order -PrimaryKey 和一个DateTime类型的CreationDate Filling those two columns from multiple threads is not a problem从多个线程填充这两列不是问题
  • when retrieving all Orders for a specific SupplierId , sort the result list in ascending order by CreationDate检索特定SupplierId所有Orders时,按CreationDate按升序对结果列表进行排序
  • when iterating over the result list using (for example) a for-loop, then the counter is the desired sequential counter当使用(例如)for 循环迭代结果列表时,计数器是所需的顺序计数器
  • as an alternative to the EF solution, the creation of the sequential counter can stay in SQL - create a view or stored procedure for the Order items and use ROW_NUMBER to create the artificial sequential count, after grouping the items over SupplierId and sorting on CreationDate作为 EF 解决方案的替代方案,顺序计数器的创建可以保留在 SQL 中 - 为Order项创建视图或存储过程,并使用ROW_NUMBER创建人工顺序计数,然后根据SupplierId对项目进行分组并在CreationDate排序

Reading the database from multiple threads (and creating the counter in every thread) is again not a problem any more.从多个线程读取数据库(并在每个线程中创建计数器)再次不再是问题。


Answer of the first question:第一个问题的答案:

You are almost there.你快到了。 You need to normalize your data model a little bit more.您需要再规范化您的数据模型。 This is a common scenario in which you want to minimize redundancy of the data and at the same time still maintain a meaningful relation (without the use of triggers).这是一种常见的场景,您希望最大限度地减少数据冗余,同时仍然保持有意义的关系(不使用触发器)。

One possible solution would be to create a Car_has_Part -Table in order to represent the relation between a Car and a Part entity:一种可能的解决方案是创建一个Car_has_Part来表示CarPart实体之间的关系:

| Car_has_Part |
----------------
| PartId       |
| CarId        |

The primary key of the Car_has_Part table is a composite primary key consisting of CarId + PartId which is unique and at the same time you avoid data duplication. Car_has_Part表的主键是CarId + PartId组成的复合主键,它是唯一的,同时避免数据重复。

In your example in the Parts table the Doors part is repeated for every Car .Parts表中的示例中, Doors部分对每个Car重复。 Using this intermediate table the data is not duplicated and you have a proper relation.使用此中间表,数据不会重复,并且您有适当的关系。

Your new data model could look like this:您的新数据模型可能如下所示:

| Car  |    | Car_has_Part |    | Part   |
-------     ----------------    ----------
|CarId |    | PartId       |    | PartId |
|Model |    |              |    | Descr  |
| etc. |    | CarId        |    | etc.   |

This model allow resp.这个模型允许resp。 covers the specified requirements:涵盖指定的要求:

I need to be able to create a counter, like an auto incremented number, to count the parts for each car.我需要能够创建一个计数器,比如一个自动递增的数字,来计算每辆车的零件。 Car 1, could have parts 1, 2, 3... and Car 2 would also have parts 1, 2, 5, 7... (some parts might be deleted along the way).汽车 1 可能有第 1、2、3 部分……而汽车 2 也有第 1、2、5、7 部分……(某些部分可能会在此过程中被删除)。

Select all PartId 's from the Car_has_Part table over CarId .Car_has_Part表中选择所有PartIdCarId

Each part HAS to be counted separately for its related car.每个部分都必须单独计算其相关汽车。 That's the base requirement.这是基本要求。

Same as above (without data duplication like in your example).与上面相同(没有像您的示例中那样的数据重复)。 Adding resp.添加响应。 removing a relation or modifying a part name has also become easier - you need to update only one row in the Parts table and the change is reflected for every car.删除关系或修改零件名称也变得更容易 - 您只需更新Parts表中的一行,更改就会反映在每辆车上。

About the triggers question - you can only create a trigger with EF (using code first approach).关于触发器问题 - 您只能使用 EF 创建触发器(使用代码优先方法)。 Regarding execution - triggers are always executed in the database and EF can't control trigger execution (you can certainly enable/disable trigger using raw SQL queries , but if I understand your question correctly this is not what you want).关于执行 - 触发器始终在数据库中执行,EF 无法控制触发器执行(您当然可以使用原始 SQL 查询启用/禁用触发器,但如果我正确理解您的问题,这不是您想要的)。

This is not a real world example.这不是真实世界的例子。 That's why you are struggling.这就是为什么你在挣扎。 For an example, A real world parts entity is lot more complicated than that.例如,真实世界的零件实体比这复杂得多。 A real world part will have a ManufacturerId (BMW, Audi etc), PartNumber(B4-773284-YT), VehicleModelId (AUDI A4 etc), Description, ManufacturerYear so on and so forth.真实世界的零件将具有制造商 ID(宝马、奥迪等)、零件编号(B4-773284-YT)、车辆型号 ID(奥迪 A4 等)、描述、制造商年份等。 Usually when it comes to parts entities, we use a concatanated primary key on ManufacturerId and PartNumber.通常,当涉及到零件实体时,我们在制造商 ID 和零件编号上使用连接的主键。

Same with your car table.和你的汽车桌一样。 It's not a real world example too.这也不是现实世界的例子。 Car entity should have a VIN number, which is unique.汽车实体应该有一个唯一的 VIN 号。 When you say each part is specific, you are not talking about Part entity.当您说每个部分都是特定的时,您并不是在谈论 Part 实体。 You are talking about PartInventory entity.您正在谈论 PartInventory 实体。 PartInventory has a unique serial number (barcode) for each part. PartInventory 的每个零件都有一个唯一的序列号(条形码)。 So every single part can be identified uniquely.因此,每个零件都可以唯一标识。 When you attach a part to a vehicle, you are not just attaching a Part, you are actually attaching a PartInventory item, which is recognizable by a unique serial number.当您将零件附加到车辆时,您不仅仅是在附加零件,您实际上是在附加一个 PartInventory 项目,它可以通过唯一的序列号进行识别。

Once the partInventory item is attached to a vehicle, it becomes a fitted part item of the vehicle.一旦 partInventory 项目附加到车辆,它就会成为车辆的装配部件项目。 Which means the part gets transferred to VehicleParts table.这意味着零件被转移到VehicleParts表。

Unfortunately I see a lot of gaps in your vehicle industry domain knowledge.不幸的是,我发现您的汽车行业领域知识存在很多空白。 We develop systems to address real world problems.我们开发系统来解决现实世界的问题。 When you try to address hypothetical problems, you run in to this kind of issues.当您尝试解决假设性问题时,您会遇到此类问题。 That leads to wasting lot of other peoples time who are trying to help you out.这会导致浪费很多其他试图帮助你的人的时间。

After investigating some possible approaches (see links at the bottom), I've came out with a very basic solution, with the help of @Vladimir Baranov.在调查了一些可能的方法后(请参阅底部的链接),在 @Vladimir Baranov 的帮助下,我提出了一个非常基本的解决方案。

I've ruled out using SqlServer triggers / Stored Procedures .我已经排除使用SqlServer 触发器/存储过程 They seemed hard to implement in conjunction with Entity Framework, and they seem to me like an Over-Kill in this scenario.它们似乎很难与 Entity Framework 一起实现,在我看来,它们在这种情况下就像一个 Over-Kill。

I've also ruled out the Optimistic Concurrency approach (using a concurrency token), because in this scenario, the counters cannot be updated simultaneously.我还排除了Optimistic Concurrency方法(使用并发令牌),因为在这种情况下,计数器不能同时更新。 They only get updated after a successful insertion to the orders table.它们只有在成功插入到 orders 表后才会更新。

My orders table looks like that.我的订单表看起来像那样。 I've added a unique constraint on the OrderId , SupplierId and OrderCountForSupplier trio, so insertion of the same order count for a supplier would fail.我在OrderIdSupplierIdOrderCountForSupplier三重奏上添加了唯一约束,因此为供应商插入相同的订单计数将失败。

在此处输入图片说明

I've indeed used a counters table, from which I can take the latest counter - for each of the suppliers.我确实使用了一个计数器表,从中我可以为每个供应商获取最新的计数器。

Supplier Order Counters Table

| SupplierId | OrderCountForSupplier
------------------------
| 54654         | 3
| 78787         | 2
| 99666         | 4

These are the steps:这些是步骤:

  1. Get the current supplier orders counter.获取当前供应商订单计数器。
  2. Try insert a new order for the supplier, using the current counter + 1.尝试使用当前计数器 + 1 为供应商插入新订单。
  3. If the insertion goes ok => Increase the orders counter for this supplier, on the supplier counters table.如果插入正常 => 在供应商计数器表上增加此供应商的订单计数器。

    If insertion goes wrong, and we get an error stating the has been a violation of the constraint (same order count, which already exists): Try 2 more times to get the current counter, and try inserting the order again.如果插入出错,并且我们收到一个错误,说明违反了约束(相同的订单计数,已经存在):再尝试 2 次以获取当前计数器,然后再次尝试插入订单。

The Code:编码:

   public class SupplierRepository
   {
        private MyContext _context;
        private Supplier _supplier;

        public SupplierRepository(int supplierId)
        {
            _context = new MyContext();
            _supplier = context.Suppliers.Single(x => x.SupplierId == supplierId);
        }

        // Retrieve the latest counter for a supplier
        public SupplierCounter GetCounter()
        {
            var counterEntity = _context.SupplierCounters.Single(x => x.SupplierId == _supplier.SupplierId);
            return counterEntity;
        }

        // Adding a supplier
        public void AddSupplier(Order order)
        {
            int retries = 3;

            while (retries > 0)
            {
                SupplierCounter currentCounter = GetCounter();
                try
                {
                    // Set the current counter into the order object
                    _order.OrderCountForSupplier = currentCounter.OrderCountForSupplier;
                    _context.Add(order);                        
                    // Success! update the counter (+1) and then break out of the while loop.
                    currentCounter.OrderCountForSupplier += 1;  
                    // I'M CALLING `SAVECHANGES` AFTER ADDING AN ORDER AND INCREASING THE COUNTER, SO THEY WOULD  BE IN THE SAME TRANSACTION. 
                    // THIS WOULD PREVENT A SCENARIO WHERE THE ORDER IS ADDED AND THE COUNTER IS NOT INCREMENTED.
                    _context.SaveChanges();
                    break;
                }
                catch (SqlException ex)
                {
                    if (ex.Number == 2627) // Violating unique constraint
                    {
                        --retries;
                    }
                }
            }
        }
    }

Some useful links:一些有用的链接:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM