Using StampAll To Force Field-Level Integrity of a Response Document
Let if be known that I'm not so proud I can't let a paying customer teach me how to do the job for them. Nor for one second do I think that I know all there is to know about Notes. What follows is a good case in point.
Remember the debate about how to design a university blogging tool from April? The solution we ended up using, which I'll discuss in more detail at some point soon, uses Readers fields. It soon became apparent that not only did the main blog entry need a Readers field, but each comment needed a readers field with the same values.
Inheriting the article's set of readers in to the child comment document at point of creation is trivial. It's keeping the values in all the comments inline with the parent when the values in the parent changes that's not so easy. It's at the point we try and mimic relational behaviour that code gets messy. Normally I'd opt for something like this in the WQS agent:
Dim comments As NotesDocumentCollection Dim comment as NotesDocument Dim readeritem as NotesItem Set comments = Web.Document.responses Set comment = comments.GetFirstDocument While not comment is nothing call comment.RemoveItem("DocReaders") set readeritem = New NotesItem(comment, "DocReaders", web.document.DocReaders, READERS) Call comment.Save(True, True) set comment = comments.GetNextDocument(comment) Wend
As I said though, the customer made this a whole lot easier for me by suggesting I look at using the StampAll method of the NotesDocumentCollection object. The above code thus becomes a single-liner:
Call Web.Document.Responses.StampAll("DocReaders", Web.Document.DocReaders)
This, to me at least, is a revelation. What about you? All this time with Notes and I've never once took the time to learn how to make use of the StampAll method, which I had heard of, but never used until last week. Something tells me I'll be using it a lot more in the future. Thanks Mark!
Is there anything I should know about the method? Is it as foolproof as it seems? What happens when you attempt multiple calls to StampAll on the same NotesDocumentCollection? Conflicts?
I haven't used NotesDocumentCollection.StampAll either, and haven't had reason to do so. My question would be: Does it retain the field flags, in this case the IsReaders=True, so that it doesn't just replace the field with a standard text field?
Use is all the time!
We use StampAll whenever we want to modifiy a field in all the documents in a collection.
We have never had any problems using it, even when called twice on the same collection (although if I want to modify more than one field I usually write the full "while" loop.
Just two things you should know (and you probably already knew).
You were removing and creating a field in your original code, you could have used just one line of code with "doc.replaceItemValue" (much simpler to read if you are modifying a few items).
Of course, doc.replaceItemValue creates the item, if it didn't exist, but it would be a normal field (not a readers field). *You would have the same problem with stampAll*, if the field doesn't exist already.
Peter. It seems to retain the flag. Probably because we're passing the method an item rather than a value, so it takes the item properties with it.
Salva. Your second point explains the first.
Hi.
In the case you have to modify a Readers field and an Authors field in the same document, you must use the loop, because if you are going to change this fields without your own name in the list, the second one will fail. If none of the fields are Readers or Authors, you can use StampAll as you like. This applies to this other methods of NotesDocumentCollection: PutAllInFolder, RemoveAll, RemoveAllFromFolder and UpdateAll.
Sadur.
Good point Sadur. In my case the code is being run in an agent signed by an ID with the "admin" role, which is included by default in all readers fields.
Hi Jake, I broke out in sweat when I read this post. Oh no I thought, I've been using a recursive function do this for years and here you have a lovely one line solution.
I had to check my work. I looked in the Notes help first and realised why I had used a recursive function:
"This property returns only immediate responses to a document...Responses-to-responses are not included."
Great suggestion though, I never realised you could chain stampAll onto .responses
Hi Ed. That's a shame. I didn't realise that. It works in my scenario as the parent/child relationship is a blog/comment one and - like here - it's only ever one level deep.
Maybe a recursive loops of .responses of each document in each collection would work?
Jake
If I had to do this with lots of recursive nested response and response to response documents, I would seed each document with a field containing the UNID of the root document at the time they are created. Then, you could select on the hidden field, and stamp in one easy operation.
db.Search("RootUNID = " + UNID, nothing, 0).StampAll("Whatever", value)
However, it wouldn't matter very much if there were only a few.
No worries Jake, it's stay a useful tip.
Hi Scott, I'm not keen on storing data on documents that can be calculated. The recursive loop is easy peasey. Here's an example:
Sub DoKids (parent As NotesDocument, parentTitle As String)
' Recursive loop to get response to responses and so on
' We use this loop update descendant documents if the title of the main document has changed
Dim collection As NotesDocumentCollection
Dim kid As NotesDocument
Dim item As NotesItem
Dim i As Integer
Set collection = parent.Responses
For i = 1 To collection.Count
Set kid = collection.GetNthDocument(i)
kid.inheritedTitle = parentTitle
Call kid.Save(False,False)
Call doKids(Kid, parentTitle)
Next
End Sub
Jake and Peter: StampAll retains the item flags, but it retains only the flags which were originally set on the response document.
You're not actually passing an item, but only an item value. In my experience this method retains item flags, but you cannot overwrite them. If the response already has a field DocReaders which is a plain text field, it won't transform the field in a readers field. Also, if the response document does not contain an item DocReaders, it will not create a readers field, but a plain text field.
Not only does StampAll retain field flags, it's also MANY times faster than opening, modifying and saving individual documents. So you not only get the benefit of more compact and maintainable code, you also get the benefit of speed and efficiency.
@scott, Isn't that db.search going to be expensive, just for the sake of a few lines of code? I'd be inclined to go with a recursive method, but using stampAll to do the update actual update. Here's a java version.
public void stampDecendants(Document doc, String itemName, Object value) throws NotesException{
DocumentCollection children=doc.getResponses();
if (children.getCount()>0){
children.stampAll(itemName, value);
Document child=children.getFirstDocument();
while(child!=null){
stampDecendants(doc, itemName, value);
child.recycle();
child=children.getNextDocument();
}
}
}
Caveat: Obviously all this stuff can be very implementation specific, your mileage may vary, may contain nuts, etc.
@Nico -- that's what "retains" means.
@Stan: Thanks for updating my english language skills... It wasn't clear to me in combination with Jake's assumption that you're passing an item rather than an item value, which in my opinion is not the case.
Ed Lee:
While reading your code I saw that you used the coll.GetNthDocument() method.
As far as I know this method is not efficient at all. I have been told that every time it's called it starts from the first document, going to the next document and so fourth.
Looking at how many times it will iterate over a collection would be something eq. to (n * (n +1)) / 2 where n is the amount of documents.
Please correct me if I am wrong.
Soren, you're exactly right, this: is much faster:
Set kid = collection.getFirstDocument()
While Not (kid Is Nothing)
'Do stuff to kid
Set kid = collection.getNextDocument(kid)
Wend
Soren/Tim, you are both right. Looks like I need to make an update to my function.
Thanks for pointing that out.
I thought I had posted this earlier, but...
The whole discussion we are having only makes sense when there are a LOT of documents; otherwise, any code is quick enough. It is in these circumstances that I like to precompute even though it is easy enough to calculate programmatically. Also, having a root reference is probably useful for many other things, and it is static data (root never changes) so there is no downside in this case. (In my opinion, the overhead of creating a document is so large a single small item being added to it is trivial).
In terms of performance, db.Search is very very fast -assuming- the db is full text indexed. For example, I just did a formula based db.Search in my organization's NAB which contains 28k+ documents, and in Notes debug mode it takes only a second or two.
(This wasn't just to test the idea; I am working on an administrative tool which manipulates group documents automatically, and one thing I needed to determine is every single group document--in a set of NABs through directory assistance--which reference a given group. This @formula uses @uppercase twice and an @contains so it is not remarkably fast to start with, and the overall db.Search is still very fast, which is a credit to the formula language implementation and the Domino full text indexing engine).
It's interesting to hear about the performance side of things, but my original excitement at finding out about the technique was because it's so much *easier* to code ;o)
Easy to code and maintain is -usually- the most important factor, I agree :-)
Although it might not be applicable in this case, I like the db.Search method because you don't need to set up a custom view to drive anything (and so there is no view for another developer to accidentally delete or modify, thus breaking the code that relies on it...)
@Scott, As I say, best performance with this stuff can be very specific. I have to say that I didn't think that db.search used an FT index if availible, so I'll have to play with that.
There's a good article on developerworks about this: {Link}
Interestingly that article says there was a bug in some older (pre 6.5) point releases of domino that made stampAll slow.
Stampall is cool, the only caveat is that stampall forces a save of each document. so if you are changing many items on a collection of documents, you are probably quicker in doing it the old-fashioned way. Single field though, it's a nice one-liner
Andrew
Salva mentioned using a while loop when there's more than one thing to stamp - I did an analysis ages ago and found that for fewer than 5 fields you're better off doing multiple stampAll calls.
I did some analysis and graphs on my site at the time if anyone is interested: {Link}
Just wanted to mention that notesViewEntryCollection.StampAll is also available. I haven't worked out if it offers a better performing solution for recursing levels of response than NotesDocumentCollection.StampAll but its an alternative. The FTSearch method in notesViewEntryCollection operates within the view so it should offer a simpler query.
StampAll translates into a single Notes API call, NSFDbStampNotes, as opposed to the more conventional sequence of calls that would be necessary to open each note, read and update the item, and save the note. But furthermore, my understanding is that this call "cheats" and bypasses layers that the conventional API calls have to go through.
BTW: I believe NSFDbStampNotes was added to the API (a long time ago) to get the fastest possible performance for formula agents that did simple FIELD foo := "bar" assignments.
Ouch, via Carl {Link} an issue with using db.search() that might be of interest given this little discussion.
Technote: {Link}
db.search() returns with all matching docs in the collection, even if max docs argument is specified. The count property returns the correct number. What happens if you do a stampAll() on the returned collection? I'd want to check that before using it.
P.S. I know that in the db.search examples earlier this isn't an issue, but I thought it was worth mentioning.
Did you ever consider creating a group for each student that has a name that never changes so that you can make changes to the readers in the group without ever editing the original document or it's children? Building the group hierarchies is a pain but the Access control is a breeze later...
Hi Jake,
I have always use StampAll, but about your code, even without StampAll, to avoid the item.remove, you can do this :
While not comment is nothing
call comment.ReplaceItemValue("DocReaders", web.document.DocReaders)
comment.GetFirstItem("DocReaders").IsReaders = true
Call comment.Save(True, True)
set comment = comments.GetNextDocument(comment)
Wend
Obviously, the web.document.DocReaders content should be in canonicalize format.
Best Regards and many thanks for your great website.
Fabrice
I've never done that because I've never needed to to update a single field on the child docs. I've needed to do multiple. For that, I assumed that StampAll would be much less efficient than looping because changing multiple fields and saving once would be better.
What I'd love is a NotesDocumentCollection.AutoSave property (default true) and a NotesDocumentCollection.Save method. Ignoring these would be current functionality, but by turning AutoSave to false you could queue several changes to the collection and then commit them in batch.
Setting the isReaders flag - no need for the extra getFirstItem since the replaceItemValue alresdy returns the item, so:
doc.replaceItemValue("readers","[me]").isReaders = True
Another oneliner !