|
| 1 | +## Database client |
| 2 | + |
| 3 | +This is where the magic happens! The `Pyneo4jClient` is the main entry point for interacting with the database. It handles all the heavy lifting for you and your models. Because of this, we have to always have at least one client initialized before doing anything else. |
| 4 | + |
| 5 | +### Connecting to the database |
| 6 | + |
| 7 | +Before you can run any queries, you have to connect to a database. This is done by calling the `connect()` method of the `Pyneo4jClient` instance. The `connect()` method takes a few arguments: |
| 8 | + |
| 9 | +- `uri`: The connection URI to the database. |
| 10 | +- `skip_constraints`: Whether the client should skip creating any constraints defined on models when registering them. Defaults to `False`. |
| 11 | +- `skip_indexes`: Whether the client should skip creating any indexes defined on models when registering them. Defaults to `False`. |
| 12 | +- `*args`: Additional arguments that are passed directly to Neo4j's `AsyncDriver.driver()` method. |
| 13 | +- `**kwargs`: Additional keyword arguments that are passed directly to Neo4j's `AsyncDriver.driver()` method. |
| 14 | + |
| 15 | +```python |
| 16 | +from pyneo4j_ogm import Pyneo4jClient |
| 17 | + |
| 18 | +client = Pyneo4jClient() |
| 19 | +await client.connect(uri="<connection-uri-to-database>", auth=("<username>", "<password>"), max_connection_pool_size=10, ...) |
| 20 | + |
| 21 | +## Or chained right after the instantiation of the class |
| 22 | +client = await Pyneo4jClient().connect(uri="<connection-uri-to-database>", auth=("<username>", "<password>"), max_connection_pool_size=10, ...) |
| 23 | +``` |
| 24 | + |
| 25 | +After connecting the client, you will be able to run any cypher queries against the database. Should you try to run a query without connecting to a database first (it happens to the best of us), you will get a `NotConnectedToDatabase` exception. |
| 26 | + |
| 27 | +### Closing an existing connection |
| 28 | + |
| 29 | +Connections can explicitly be closed by calling the `close()` method. This will close the connection to the database and free up any resources used by the client. Remember to always close your connections when you are done with them! |
| 30 | + |
| 31 | +```python |
| 32 | +## Do some heavy-duty work... |
| 33 | + |
| 34 | +## Finally done, so we close the connection to the database. |
| 35 | +await client.close() |
| 36 | +``` |
| 37 | + |
| 38 | +Once you closed the client, it will be seen as `disconnected` and if you try to run any further queries with it, you will get a `NotConnectedToDatabase` exception |
| 39 | + |
| 40 | +### Registering models |
| 41 | + |
| 42 | +Models are a core feature of pyneo4j-ogm, and therefore you probably want to use some. But to work with them, they have to be registered with the client by calling the `register_models()` method and passing in your models as a list: |
| 43 | + |
| 44 | +```python |
| 45 | +## Create a new client instance and connect ... |
| 46 | + |
| 47 | +await client.register_models([Developer, Coffee, Consumed]) |
| 48 | +``` |
| 49 | + |
| 50 | +This is a crucial step, because if you don't register your models with the client, you won't be able to work with them in any way. Should you try to work with a model that has not been registered, you will get a `UnregisteredModel` exception. This exception also gets raised if a database model defines a relationship-property with other (unregistered) models as a target or relationship model and then runs a query with said relationship-property. |
| 51 | + |
| 52 | +If you have defined any indexes or constraints on your models, they will be created automatically when registering them. You can prevent this behavior by passing `skip_constraints=True` or `skip_indexes=True` to the `connect()` method. If you do this, you will have to create the indexes and constraints yourself. |
| 53 | + |
| 54 | +> **Note**: If you don't register your models with the client, you will still be able to run cypher queries directly with the client, but you will `lose automatic model resolution` from queries. This means that, instead of resolved models, the raw Neo4j query results are returned. |
| 55 | +
|
| 56 | +### Executing Cypher queries |
| 57 | + |
| 58 | +Models aren't the only things capable of running queries. The client can also be used to run queries, with some additional functionality to make your life easier. |
| 59 | + |
| 60 | +Node- and RelationshipModels provide many methods for commonly used cypher queries, but sometimes you might want to execute a custom cypher with more complex logic. For this purpose, the client instance provides a `cypher()` method that allows you to execute custom cypher queries. The `cypher()` method takes three arguments: |
| 61 | + |
| 62 | +- `query`: The cypher query to execute. |
| 63 | +- `parameters`: A dictionary containing the parameters to pass to the query. |
| 64 | +- `resolve_models`: Whether the client should try to resolve the models from the query results. Defaults to `True`. |
| 65 | + |
| 66 | +This method will always return a tuple containing a list of results and a list of variables returned by the query. Internally, the client uses the `.values()` method of the Neo4j driver to get the results of the query. |
| 67 | + |
| 68 | +> **Note:** If no models have been registered with the client and resolve_models is set to True, the client will not raise any exceptions but rather return the raw query results. |
| 69 | +
|
| 70 | +Here is an example of how to execute a custom cypher query: |
| 71 | + |
| 72 | +```python |
| 73 | +results, meta = await client.cypher( |
| 74 | + query="CREATE (d:Developer {uid: '553ac2c9-7b2d-404e-8271-40426ae80de0', name: 'John', age: 25}) RETURN d.name as developer_name, d.age", |
| 75 | + parameters={"name": "John Doe"}, |
| 76 | + resolve_models=False, ## Explicitly disable model resolution |
| 77 | +) |
| 78 | + |
| 79 | +print(results) ## [["John", 25]] |
| 80 | +print(meta) ## ["developer_name", "d.age"] |
| 81 | +``` |
| 82 | + |
| 83 | +### Batching cypher queries |
| 84 | + |
| 85 | +We provide an easy way to batch multiple database queries together, regardless of whether you are using the client directly or via a model method. To do this you can use the `batch()` method, which has to be called with a asynchronous context manager like in the following example: |
| 86 | + |
| 87 | +```python |
| 88 | +async with client.batch(): |
| 89 | + ## All queries executed inside the context manager will be batched into a single transaction |
| 90 | + ## and executed once the context manager exits. If any of the queries fail, the whole transaction |
| 91 | + ## will be rolled back. |
| 92 | + await client.cypher( |
| 93 | + query="CREATE (d:Developer {uid: $uid, name: $name, age: $age})", |
| 94 | + parameters={"uid": "553ac2c9-7b2d-404e-8271-40426ae80de0", "name": "John Doe", "age": 25}, |
| 95 | + ) |
| 96 | + await client.cypher( |
| 97 | + query="CREATE (c:Coffee {flavour: $flavour, milk: $milk, sugar: $sugar})", |
| 98 | + parameters={"flavour": "Espresso", "milk": False, "sugar": False}, |
| 99 | + ) |
| 100 | + |
| 101 | + ## Model queries also can be batched together without any extra work! |
| 102 | + coffee = await Coffee(flavour="Americano", milk=False, sugar=False).create() |
| 103 | +``` |
| 104 | + |
| 105 | +You can batch anything that runs a query, be that a model method, a custom query or a relationship-property method. If any of the queries fail, the whole transaction will be rolled back and an exception will be raised. |
| 106 | + |
| 107 | +### Using bookmarks (Enterprise Edition only) |
| 108 | + |
| 109 | +If you are using the Enterprise Edition of Neo4j, you can use bookmarks to keep track of the last transaction that has been committed. The client provides a `last_bookmarks` property that allows you to get the bookmarks from the last session. These bookmarks can be used in combination with the `use_bookmarks()` method. Like the `batch()` method, the `use_bookmarks()` method has to be called with a context manager. All queries run inside the context manager will use the bookmarks passed to the `use_bookmarks()` method. Here is an example of how to use bookmarks: |
| 110 | + |
| 111 | +```python |
| 112 | +## Create a new node and get the bookmarks from the last session |
| 113 | +await client.cypher("CREATE (d:Developer {name: 'John Doe', age: 25})") |
| 114 | +bookmarks = client.last_bookmarks |
| 115 | + |
| 116 | +## Create another node, but this time don't get the bookmark |
| 117 | +## When we use the bookmarks from the last session, this node will not be visible |
| 118 | +await client.cypher("CREATE (c:Coffee {flavour: 'Espresso', milk: False, sugar: False})") |
| 119 | + |
| 120 | +with client.use_bookmarks(bookmarks=bookmarks): |
| 121 | + ## All queries executed inside the context manager will use the bookmarks |
| 122 | + ## passed to the `use_bookmarks()` method. |
| 123 | + |
| 124 | + ## Here we will only see the node created in the first query |
| 125 | + results, meta = await client.cypher("MATCH (n) RETURN n") |
| 126 | + |
| 127 | + ## Model queries also can be batched together without any extra work! |
| 128 | + ## This will return no results, since the coffee node was created after |
| 129 | + ## the bookmarks were taken. |
| 130 | + coffee = await Coffee.find_many() |
| 131 | + print(coffee) ## [] |
| 132 | +``` |
| 133 | + |
| 134 | +### Manual indexing and constraints |
| 135 | + |
| 136 | +Most of the time, the creation of indexes/constraints will be handled by the models themselves. But it can still be handy to have a simple way of creating new ones. This is where the `create_lookup_index()`, `create_range_index`, `create_text_index`, `create_point_index` and `create_uniqueness_constraint()` methods come in. |
| 137 | + |
| 138 | +First, let's take a look at how to create a custom index in the database. The `create_range_index`, `create_text_index` and `create_point_index` methods take a few arguments: |
| 139 | + |
| 140 | +- `name`: The name of the index to create (Make sure this is unique!). |
| 141 | +- `entity_type`: The entity type the index is created for. Can be either **EntityType.NODE** or **EntityType.RELATIONSHIP**. |
| 142 | +- `properties`: A list of properties to create the index for. |
| 143 | +- `labels_or_type`: The node labels or relationship type the index is created for. |
| 144 | + |
| 145 | +The `create_lookup_index()` takes the same arguments, except for the `labels_or_type` and `properties` arguments. |
| 146 | + |
| 147 | +The `create_uniqueness_constraint()` method also takes similar arguments. |
| 148 | + |
| 149 | +- `name`: The name of the constraint to create. |
| 150 | +- `entity_type`: The entity type the constraint is created for. Can be either **EntityType.NODE** or **EntityType.RELATIONSHIP**. |
| 151 | +- `properties`: A list of properties to create the constraint for. |
| 152 | +- `labels_or_type`: The node labels or relationship type the constraint is created for. |
| 153 | + |
| 154 | +Here is an example of how to use the methods: |
| 155 | + |
| 156 | +```python |
| 157 | +## Creates a `RANGE` index for a `Coffee's` `sugar` and `flavour` properties |
| 158 | +await client.create_range_index("hot_beverage_index", EntityType.NODE, ["sugar", "flavour"], ["Beverage", "Hot"]) |
| 159 | + |
| 160 | +## Creates a UNIQUENESS constraint for a `Developer's` `uid` property |
| 161 | +await client.create_uniqueness_constraint("developer_constraint", EntityType.NODE, ["uid"], ["Developer"]) |
| 162 | +``` |
| 163 | + |
| 164 | +### Client utilities |
| 165 | + |
| 166 | +The client also provides some additional utility methods, which mostly exist for convenience when writing tests or setting up environments: |
| 167 | + |
| 168 | +- `is_connected()`: Returns whether the client is currently connected to a database. |
| 169 | +- `drop_nodes()`: Drops all nodes from the database. |
| 170 | +- `drop_constraints()`: Drops all constraints from the database. |
| 171 | +- `drop_indexes()`: Drops all indexes from the database. |
0 commit comments