Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create new Document without a HTTP request #24

Closed
adrienverge opened this issue Mar 30, 2020 · 4 comments
Closed

Create new Document without a HTTP request #24

adrienverge opened this issue Mar 30, 2020 · 4 comments

Comments

@adrienverge
Copy link
Contributor

This follows #23 (comment).

It can be needed to create a document (without caring whether it exists or not) without having to issue an unneeded HTTP call (HEAD or GET). For example, currently we need a lot of Python lines:

# This document will be immutable, we just want to create it if not existing
# yet; using only one HTTP call for performance reasons.
db = Database(couch, 'db')
doc = Document(db, 'id')
doc.update({'key': 'val'})
try:
    await doc.save()
except aiocouch.exception.ConflictError:
    pass

This could be made easier with aiocouch in two ways:

  1. Allow setting a Document instance with a value, without fetching it from CouchDB, in one line.
    E.g.

    doc = Document(db, 'id', init={'key': 'val'})

    ... or without using the Document class directly:

    doc = await db.create('id', init={'key': 'val'})

    ... or even better, without await, because it gives the impression that I/O will be done, which is exactly what we don't want:

    doc = db.init_doc('id', {'key': 'val'})
    # or
    doc = db.init('id', {'key': 'val'})
    # or
    doc = db.new_doc('id', {'key': 'val'})
  2. Allow saving it if it doesn't exists already. Something similar to discard_changes and exists_ok, although these namings would be ambiguous here, I think.

    await doc.save(fail_on_conflict=False)
    # or
    await doc.save(ignore_conflict=True)
    # or
    await doc.save(ignore_if_exists=True)

PS: Similarly, it would be useful to be able to create a Database object without using the Database class (= "recommended API") but without making a HTTP call.

@bmario
Copy link
Member

bmario commented Mar 31, 2020

Thanks for providing those cases.

I have one understanding problem with your example, though. If I understand correctly, you just want to make sure that the document exists and, if not, create it with default data.

Speaking in HTTP requests, what you propose is an unconditional PUT /db/doc {data} and ignoring the possible conflicts. Wouldn't it be better to issue first a HEAD /db/doc, and only if that fails to do the PUT? In case the document exists, you saved the transmission of data. In the case, the document doesn't exist yet, you had one "useless" HEAD.

It seems that what is better depends on the latency to bandwidth ratio, the document size and the probability that a document exists.

Side note: You can use suppress in your original code to shorten it a bit:

with contextlib.suppress(aiocouch.exception.ConflictError):
    await doc.save()

@adrienverge
Copy link
Contributor Author

Hello @bmario, thanks for your answer! Just to be 100% clear: there are 2 different features shown by my single use case:

  1. creating and initializing a doc in one line, e.g. doc = Document(db, 'id', init={'key': 'val'}),
  2. (what you pointed out correctly) make sure that the document exists and, if not, create it with default data.

About 2, I understand your point: better do a preliminary (but lightweight) HEAD request, rather than a large PUT that could be rejected with 409.

But this does not work in all cases. For example in mine, I have to create a new (small) document, and this succeeds 99% of the times. However sometimes I can fail with a ConflictError because of (rare) concurrency problems. In these conditions, I don't want to issue a "most-of-the-time useless" HEAD request, given that my CouchDB servers are already under pressure.

@bmario
Copy link
Member

bmario commented Apr 1, 2020

Thanks for the clarification.

1. creating and initializing a doc in one line, e.g. `doc = Document(db, 'id', init={'key': 'val'})`,

I added the proposed parameter in cda57f9. It's called data though to be in line with the data member of the Document.


I also thought a lot about the other points you brought up here.

It can be needed to create a document (without caring whether it exists or not) without having to issue an unneeded HTTP call (HEAD or GET).

From an API standpoint, I think directly use the respective constructors is indeed the way to go here. This separation allows us to clearly distinguish between the "batteries-included"-interface, which will raise errors as soon as possible, and the "I-know-what-I-am-doing" interface to optimize performance. Plus, the proposed new_doc interface would only be a thin wrapper around that anyways.

Similarly, it would be useful to be able to create a Database object without using the Database class (= "recommended API") but without making a HTTP call.

The same reasoning as above, just use the class constructor.

2. Allow saving it _if it doesn't exists already_. Something similar to `discard_changes` and `exists_ok`, although these namings would be ambiguous here, I think.

I think the pythonic way to achieve that is indeed the usage of suppress. And I don't have to find a good name for the parameter. 😄


All in all, my preferred way to achieve the behavior of your initial example is this:

db = Database(couch, 'db')
doc = Document(db, 'id', data={'key': 'val'})
with suppress(ConflictError):
    await doc.save()

@adrienverge
Copy link
Contributor Author

Thanks for your answer and for cda57f9.

From an API standpoint, I think directly use the respective constructors is indeed the way to go here. This separation allows us to clearly distinguish between the "batteries-included"-interface, which will raise errors as soon as possible, and the "I-know-what-I-am-doing" interface to optimize performance. Plus, the proposed new_doc interface would only be a thin wrapper around that anyways.

Yes, I agree. No problem with using Document and Database constructors. That's what we were doing, before discovering that it wasn't the "public API" in #23 (comment). Calling these the "I-know-what-I-am-doing interface" sounds good 👍

I think the pythonic way to achieve that is indeed the usage of suppress. And I don't have to find a good name for the parameter.

Of course there are other ways using 2+ lines (like adding with suppress(aiocouch.exception.ConflictError)). But aiocouch provides helpers to avoid that, such as db.create(exists_ok=True) or doc.fetch(discard_changes=True). I just thought it would be nice to have a similar helper here.

@bmario bmario closed this as completed Apr 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants