Tds适配器仅支持on_conflict: :raise的冲突更新替代方案咨询
问题根因
Tds(Ecto适配SQL Server的适配器)当前版本确实仅支持on_conflict: :raise配置,没有实现PostgreSQL风格的on_conflict增量更新等高级能力,这是适配器本身的功能限制,和业务逻辑代码写法无关。
可落地的替代实现方案
两种方案都可以实现和教程完全一致的「商品重复加入购物车时数量自动累加」效果,可根据性能需求选择:
方案1:事务加行锁的先查后写(推荐,代码符合Ecto常规写法,维护成本低)
利用数据库事务+行级锁规避并发竞态问题,逻辑和原生UPSERT效果完全一致,代码示例:def add_item_to_cart(%Cart{} = cart, %Catalog.Product{} = product) do Repo.transaction(fn -> # 查询对应购物车商品条目时加更新锁,避免并发操作导致的数据错误 case Repo.one(from ci in CartItem, where: ci.cart_id == ^cart.id and ci.product_id == ^product.id, lock: "UPDLOCK" ) do nil -> # 条目不存在则新建初始数量为1的记录 %CartItem{quantity: 1, price_when_carted: product.price} |> CartItem.changeset(%{}) |> Ecto.Changeset.put_assoc(:cart, cart) |> Ecto.Changeset.put_assoc(:product, product) |> Repo.insert!() existing_item -> # 条目已存在则执行数量+1更新 existing_item |> Ecto.Changeset.change(quantity: existing_item.quantity + 1) |> Repo.update!() end end) end说明:SQL Server的
UPDLOCK是更新锁,会锁定查询到的行直到事务结束,能彻底避免高并发下重复插入、计数不准的问题,不需要依赖适配器的on_conflict能力。方案2:原生SQL MERGE语句实现UPSERT(追求极致性能可选)
SQL Server原生支持MERGE语法完成单语句UPSERT,可以直接通过Ecto的原生SQL接口执行,性能和数据库原生UPSERT一致,代码示例:def add_item_to_cart(%Cart{} = cart, %Catalog.Product{} = product) do merge_sql = """ MERGE cart_items AS target USING (VALUES (@cart_id, @product_id, @init_price, @init_quantity)) AS source (cart_id, product_id, price_when_carted, quantity) ON target.cart_id = source.cart_id AND target.product_id = source.product_id WHEN MATCHED THEN UPDATE SET quantity = target.quantity + 1 WHEN NOT MATCHED THEN INSERT (cart_id, product_id, price_when_carted, quantity) VALUES (source.cart_id, source.product_id, source.price_when_carted, source.quantity); """ Ecto.Adapters.SQL.query!(Repo, merge_sql, cart_id: cart.id, product_id: product.id, init_price: product.price, init_quantity: 1 ) end说明:使用时需要把SQL里的表名、字段名和实际的数据库表结构对齐,参数通过绑定传入,不存在SQL注入风险。
注意:不要使用无锁的裸先查后写逻辑,高并发场景下会出现唯一键冲突、重复购物车条目、数量计数错误等问题。
内容的提问来源于stack exchange,提问作者Erick Concepción
相关产品推荐
相关产品推荐

