Adding to list of values
I have an app where I need to keep adding new contacts to a list of contacts that belong to an association.
This list of contacts is part of the Association template. I have a choice where I pass the new contact as param, and “create this with contacts = contact::contacts”, thus adding the new contact to the list.
Of course this creates a new Association contract and archives the old one, which is perfectly fine. But is there another way to do this instead of potentially creating lots of archived Association contracts with almost the same data. Given that I will have hundreds of associations, I am trying to see if there is another approach that I should follow.
Any alternative design ideas will be appreciated. Thank you!
First, I’m going to assume you need access to the member list for some workflow, otherwise you may as well store a reference to the Association in the Member contract and avoid the list update completely.
Most ledger applications shouldn’t need to store archives, so thrashing the association list shouldn’t normally be an issue. Still, if you are updating the list sufficiently often the resulting contention may become an issue you need to resolve. In that case there are a few ways you can ameliorate problem.
Hundreds of associations is unlikely to be an issue, as I assume their relationship to Member contracts is 1:N not M:N. Only in the latter case would you start seeing contention on the ledger. A few thousand contracts in independent workflows is not really something you should be too worried about. If the Association contract is particularly large, then you might consider splitting the contract in two, linked by the contract id of the primary Association contract. This is called a contract constellation, where you have a single logical contract split into multiple concrete contracts unrelated to the underlying data model (ie. privacy, performance, storage scalability, etc).
daml 1.2
module BatchUpdateDaml
where
template Association
with
organiser : Party
bigdata : Text
where
signatory organiser
controller organiser can
postconsuming DoUpdate : ContractId Association
with
newdata : Text
do
(membersId, members) <- fetchByKey @AssociationMembers (organiser, self)
newAssociation <- create this with bigdata = newdata
exercise membersId UpdateAssociation with newAssociation
pure newAssociation
template AssociationMembers
with
organiser : Party
association : ContractId Association
members : [Text]
where
signatory organiser
key (organiser, association) : (Party, ContractId Association)
maintainer key._1
controller organiser can
UpdateAssociation : ContractId AssociationMembers
with
newAssociation : ContractId Association
do
create this with association = newAssociation
UpdateAssociationMembers : ContractId AssociationMembers
with
newMember : Text
do
fetchByKey @Member (organiser, newMember)
create this with members = newMember :: members
template Member
with
organiser : Party
name : Text
where
signatory organiser
key (organiser, member) : (Party, Text)
maintainer organiser
This becomes slightly more complicated if the membership list changes often enough that you experience contention over the list updates. In this case you should consider reifying the membership updates and batching them.
daml 1.2
module BatchUpdateDemo
where
import DA.Optional
template Association
with
organiser : Party
bigdata : Text
where
signatory organiser
controller organiser can
postconsuming DoUpdate : ContractId Association
with
newdata : Text
do
(membersId, members) <- fetchByKey @AssociationMembers (organiser, self)
newAssociation <- create this with bigdata = newdata
exercise membersId UpdateAssociation with newAssociation
pure newAssociation
template AddMemberRequest
with
organiser : Party
association : ContractId Association
member : Text
where
signatory organiser
controller organiser can
Validate : Optional (ContractId Member)
with
org : Party
ass : ContractId Association
do
assert $ org == organiser
assert $ ass == association
lookupByKey @Member (organiser, member)
template AssociationMembers
with
organiser : Party
association : ContractId Association
members : [Text]
where
signatory organiser
key (organiser, association) : (Party, ContractId Association)
maintainer key._1
controller organiser can
UpdateAssociation : ContractId AssociationMembers
with
newAssociation : ContractId Association
do
create this with association = newAssociation
UpdateAssociationMembers : ContractId AssociationMembers
with
newMembers : [ContractId AddMemberRequest]
do
memberCids <- catOptionals <$> mapA (\amr -> exercise amr Validate with org = organiser; ass = association) newMembers
newMembers <- fmap (.name) <$> mapA fetch memberCids
create this with members = newMembers <> members
template Member
with
organiser : Party
name : Text
where
signatory organiser
key (organiser, name) : (Party, Text)
maintainer key._1
(Note as written the above has an easily corrected race-condition between updating the association list and updating the association data that can result in requests being stranded. Fixing it would have distracted from the batching itself).
Batching does require adding a DAML Trigger to pass all visible requests into UpdateAssociationMembers. It also means adding a member is no longer an atomic operation, and adds a full round-trip latency to the operation. So this is something you should do only once you know you have a contention problem you need to solve. Still this approach should scale comfortably up to a few hundred contending updates per second — of course if that is a sustained rather than burst rate, at that level your other workflows will probably be experiencing starvation, and handling that will require application specific engineering.
Thank you for the detailed answer @Andrae . This solved my problem.