========================== Databases and transactions ========================== Database connections are handled with :class:`~sheraf.databases.Database` objects. Under the hood the handled objects are regular ZODB :class:`~ZODB.DB` object. .. contents:: :local: Before you edit any persistent data, you need to open a database connection, and thus you need to set up a database context. Generally you can configure your :class:`~sheraf.databases.Database` with a `zodburi `__, but if you do not pass give any database information, a temporary in-memory database will be created. Depending on the cases you might want to use: - A in-memory database with :class:`~ZODB.DemoStorage.DemoStorage`; - A file database with :class:`~ZODB.FileStorage.FileStorage.FileStorage`; - A client-server based database with ZEO :class:`~ZEO.ClientStorage.ClientStorage`; - A client-server over a PostgreSQL server with :class:`~relstorage.adapters.postgresql.adapter.PostgreSQLAdapter`. Initializing a database context is done by simply creating a :class:`~sheraf.databases.Database` object. .. code-block:: python >>> db = sheraf.Database("zeo://localhost:8000"): # doctest: +SKIP The database context is now created, but to handle data you need to open a connection to the database. Database connections ==================== The easiest way to open a connection is to use the :meth:`~sheraf.databases.Database.connection` context manager: .. code-block:: python >>> with db.connection(): # doctest: +SKIP ... m = MyModel.create() ... # do other things The :func:`~sheraf.databases.connection` shortcut allows you to not depend on your :class:`~sheraf.databases.Database` object, by just passing a database name. .. code-block:: python >>> with sheraf.connection("default"): # doctest: +SKIP ... m = MyModel.create() ... # do other things There is another shortcut: if you try to open a connection to the default database, you do not need to pass it to the :func:`~sheraf.databases.connection` function.' .. code-block:: python >>> with sheraf.connection(): # doctest: +SKIP ... m = MyModel.create() ... # do other things In a context with only one database, this generally the method most database connections are done. You can also use it as a function decorator: .. code-block:: python >>> @sheraf.connection() ... def do_thing(): # doctest: +SKIP ... m = MyModel.create() ... # do other things ... >>> do_thing() # doctest: +SKIP .. warning:: Note that by default, you cannot open two connections to the same database: .. code-block:: python >>> with sheraf.connection(): # doctest: +SKIP ... with sheraf.connection(): ... m = MyModel.create() Traceback (most recent call last): ... sheraf.exceptions.ConnectionAlreadyOpened: First connection was on ... at line ... Transactions and commits ======================== A :class:`~transaction.interfaces.ITransaction` is opened each time you open a connection to a database. If you want to validate the modifications you made on your model, you can use the ``commit`` argument: .. code-block:: python >>> with sheraf.connection(commit=True): # doctest: +SKIP ... m = MyModel.create() ... # do other things ... >>> @sheraf.connection(commit=True) ... def do_thing(): # doctest: +SKIP ... m = MyModel.create() ... # do other things ... >>> do_thing() # doctest: +SKIP Another option is to use the :func:`~sheraf.transactions.commit` shortcut: .. code-block:: python >>> with sheraf.connection(): # doctest: +SKIP ... m = MyModel.create() ... # do other things ... sheraf.commit() If you made risky modifications, for instance something with probabilities to raise :class:`~ZODB.POSException.ConflictError`, you might want to make several attempts so you can reread the data, and maybe avoid the conflict at the second try. For this you can use the :func:`~sheraf.transactions.attempt` function: .. code-block:: python >>> def do_thing(): # doctest: +SKIP ... m = MyModel.create() ... # do other things ... >>> sheraf.attempt(do_thing) # doctest: +SKIP ZConfig file ============ Instead of passing arguments to :class:`~sheraf.databases.Database`, you can configure your database connections with a configuration files. It is done through a `zodburi `__ ``zconfig://`` URI scheme. A simple example ZConfig file: .. code-block:: xml If that configuration file is located at ``/etc/myapp/zodb.conf``, use the following uri argument to initialize your object: .. code-block:: python >>> sheraf.Database("zconfig:///etc/myapp/zodb.conf") # doctest: +SKIP A ZConfig file can specify more than one database. Don't forget to specify database-name in that case to avoid conflict on name. For instance: .. code-block:: text database-name database1 database-name database2 In that case, use a URI with a fragment identifier: .. code-block:: python >>> db1 = sheraf.Database("zconfig:///etc/myapp/zodb.conf#temp1") # doctest: +SKIP >>> db2 = sheraf.Database("zconfig:///etc/myapp/zodb.conf#temp2") # doctest: +SKIP If not specified in the conf file or in the arguments passed at the initialization of the object, default zodburi values will be used: * database name: unnamed * cache size: 5000 * cache size bytes: 0 Note that arguments passed at the initialization of the object override the conf file. Modifying the data into the database is done with a context manager: .. code-block:: python >>> with sheraf.connection(database_name="database1"): # doctest: +SKIP ... # currently connected to db1 ... pass If the database name is not defined, the ``database_name`` parameter is optional. Concurrency =========== Let us see how sheraf cowboys behave in parallelize contexts. .. doctest:: :hide: >>> from tests import utils >>> persistent_dir, oldmapping_dir = utils.create_temp_directory() >>> zeo_process, zeo_port = utils.start_zeo_server(persistent_dir) >>> try: ... sheraf.Database.get().close() ... except: ... pass .. doctest:: >>> class Cowboy(sheraf.Model): ... table = "cowboys" ... gunskill = sheraf.IntegerAttribute() ... >>> db = sheraf.Database("zeo://localhost:{}".format(zeo_port)) ... >>> with sheraf.connection(commit=True): ... george = Cowboy.create() Threading --------- The :ref:`ZODB documentation ` about concurrency states that database :class:`~ZODB.Connection.Connection`, :class:`~transaction.interfaces.ITransactionManager` and :class:`~transaction.interfaces.ITransaction` are not thread-safe. However ZODB :class:`~ZODB.DB` objects can be shared between threads. This means that it is possible to create a :class:`~sheraf.databases.Database` object once, and then share it on several threads. However each thread should use its own connection context: .. doctest:: >>> import threading >>> def practice_gun(cowboy_id): ... # The database is available in children thread, but they ... # need to open their own connection contexts. ... with sheraf.connection(commit=True): ... cowboy = Cowboy.read(cowboy_id) ... cowboy.gunskill = cowboy.gunskill + 1000 ... >>> practice_session = threading.Thread(target=practice_gun, args=(george.id,)) >>> practice_session.start() >>> practice_session.join() ... >>> with sheraf.connection(): ... Cowboy.read(george.id).gunskill 1000 Opening a thread within a connection context will produce various unexpected behaviors. Multiprocessing --------------- When using multiprocessing, the behavior is a bit different. The :class:`~sheraf.database.Database` are not shared between processes. .. note:: Since Python 3.14 the default start method on Linux is ``forkserver``, which pickles the target function and its arguments. The examples below explicitly use the ``fork`` context so the functions defined in this tutorial can be used as-is. In your own code, prefer making the target function importable from the child process. .. doctest:: >>> import multiprocessing >>> mp = multiprocessing.get_context("fork") >>> practice_session = mp.Process(target=practice_gun, args=(george.id,)) >>> practice_session.start() >>> practice_session.join() >>> practice_session.exitcode 1 The connection context in the ``practice_gun`` function has raised a :class:`KeyError` exception because in this new process, no database has been defined. Fortunately there is a simple solution to this. The database needs to be redefined in the new process: .. doctest:: >>> def recreate_db_and_practice_gun(cowboy_id): ... # The database is re-created in the child process ... db = sheraf.Database("zeo://localhost:{}".format(zeo_port)) ... ... with sheraf.connection(commit=True): ... cowboy = Cowboy.read(cowboy_id) ... cowboy.gunskill = cowboy.gunskill + 1000 ... db.close() ... >>> practice_session = mp.Process(target=recreate_db_and_practice_gun, args=(george.id,)) >>> practice_session.start() >>> practice_session.join() ... >>> with sheraf.connection(): ... Cowboy.read(george.id).gunskill 2000 .. note:: Remember that :class:`~ZODB.FileStorage.FileStorage.FileStorage`, :class:`~ZODB.MappingStorage.MappingStorage` and :class:`~ZODB.DemoStorage.DemoStorage` cannot be used by several processes. .. doctest:: :hide: >>> db.close() >>> utils.stop_zeo_server(zeo_process, silent=True) >>> utils.delete_temp_directory(persistent_dir, oldmapping_dir)