Coverage for src/inheritance_calculator_core/database/neo4j_client.py: 0%
158 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 05:31 +0900
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 05:31 +0900
1"""Neo4jクライアント
3Neo4jデータベースへの接続と操作を管理するクライアント。
4"""
5from typing import Any, Dict, List, Optional, Generator
6from contextlib import contextmanager
7import logging
9from neo4j import GraphDatabase, Driver, Session, Transaction
10from neo4j.exceptions import ServiceUnavailable, AuthError, Neo4jError
12from .base import DatabaseClient
13from ..utils.exceptions import DatabaseException
14from ..utils.config import Neo4jSettings
17class Neo4jClient(DatabaseClient):
18 """
19 Neo4jデータベースクライアント
21 接続管理、トランザクション管理、クエリ実行を提供する。
22 """
24 def __init__(self, settings: Optional[Neo4jSettings] = None) -> None:
25 """
26 初期化
28 Args:
29 settings: Neo4j接続設定(Noneの場合は環境変数から取得)
30 """
31 self.logger = logging.getLogger(__name__)
33 if settings is None:
34 try:
35 settings = Neo4jSettings() # type: ignore[call-arg]
36 except Exception:
37 # テスト環境等でパスワードが設定されていない場合のフォールバック
38 raise DatabaseException("Neo4j設定の初期化に失敗しました。NEO4J_PASSWORD環境変数が設定されているか確認してください。")
40 self.uri = settings.uri
41 self.user = settings.user
42 self.password = settings.password
43 self.database = settings.database
45 self._driver: Optional[Driver] = None
46 self._session: Optional[Session] = None
47 self._transaction: Optional[Transaction] = None
49 self.logger.info(f"Neo4jClient initialized for {self.uri}")
51 def connect(self) -> None:
52 """
53 データベースに接続
55 Raises:
56 DatabaseException: 接続に失敗した場合
57 """
58 try:
59 self._driver = GraphDatabase.driver(
60 self.uri,
61 auth=(self.user, self.password),
62 encrypted=False # ローカル環境のため暗号化なし
63 )
65 # 接続テスト
66 self._driver.verify_connectivity()
68 self.logger.info("Successfully connected to Neo4j")
70 except AuthError as e:
71 self.logger.error(f"Authentication failed: {e}")
72 raise DatabaseException(f"Neo4j認証に失敗しました: {str(e)}")
73 except ServiceUnavailable as e:
74 self.logger.error(f"Neo4j service unavailable: {e}")
75 raise DatabaseException(f"Neo4jサービスに接続できません: {str(e)}")
76 except Exception as e:
77 self.logger.error(f"Failed to connect to Neo4j: {e}", exc_info=True)
78 raise DatabaseException(f"Neo4j接続エラー: {str(e)}")
80 def disconnect(self) -> None:
81 """
82 データベースから切断
83 """
84 try:
85 # トランザクションをロールバック
86 if self._transaction is not None:
87 self._transaction.rollback()
88 self._transaction = None
90 # セッションを閉じる
91 if self._session is not None:
92 self._session.close()
93 self._session = None
95 # ドライバーを閉じる
96 if self._driver is not None:
97 self._driver.close()
98 self._driver = None
100 self.logger.info("Disconnected from Neo4j")
102 except Exception as e:
103 self.logger.error(f"Error during disconnect: {e}", exc_info=True)
105 def is_connected(self) -> bool:
106 """
107 接続状態を確認
109 Returns:
110 接続中の場合True
111 """
112 if self._driver is None:
113 return False
115 try:
116 self._driver.verify_connectivity()
117 return True
118 except Exception:
119 return False
121 def health_check(self) -> bool:
122 """
123 ヘルスチェック
125 Returns:
126 データベースが正常に動作している場合True
127 """
128 if self._driver is None:
129 return False
131 try:
132 with self._driver.session(database=self.database) as session:
133 result = session.run("RETURN 1 as health")
134 record = result.single()
135 if record is None:
136 return False
137 return bool(record["health"] == 1)
138 except Exception as e:
139 self.logger.error(f"Health check failed: {e}")
140 return False
142 def begin_transaction(self) -> Transaction:
143 """
144 トランザクションを開始
146 Returns:
147 トランザクションオブジェクト
149 Raises:
150 DatabaseException: トランザクション開始に失敗した場合
151 """
152 if self._driver is None:
153 raise DatabaseException("データベースに接続されていません")
155 try:
156 if self._session is None:
157 self._session = self._driver.session(database=self.database)
159 self._transaction = self._session.begin_transaction()
160 self.logger.debug("Transaction started")
161 return self._transaction
163 except Exception as e:
164 self.logger.error(f"Failed to begin transaction: {e}", exc_info=True)
165 raise DatabaseException(f"トランザクション開始エラー: {str(e)}")
167 def commit_transaction(self, transaction: Any) -> None:
168 """
169 トランザクションをコミット
171 Args:
172 transaction: トランザクションオブジェクト
174 Raises:
175 DatabaseException: コミットに失敗した場合
176 """
177 if transaction is None:
178 raise DatabaseException("トランザクションがNullです")
180 try:
181 transaction.commit()
182 if self._transaction == transaction:
183 self._transaction = None
184 self.logger.debug("Transaction committed")
186 except Exception as e:
187 self.logger.error(f"Failed to commit transaction: {e}", exc_info=True)
188 raise DatabaseException(f"トランザクションコミットエラー: {str(e)}")
190 def rollback_transaction(self, transaction: Any) -> None:
191 """
192 トランザクションをロールバック
194 Args:
195 transaction: トランザクションオブジェクト
197 Raises:
198 DatabaseException: ロールバックに失敗した場合
199 """
200 if transaction is None:
201 raise DatabaseException("トランザクションがNullです")
203 try:
204 transaction.rollback()
205 if self._transaction == transaction:
206 self._transaction = None
207 self.logger.debug("Transaction rolled back")
209 except Exception as e:
210 self.logger.error(f"Failed to rollback transaction: {e}", exc_info=True)
211 raise DatabaseException(f"トランザクションロールバックエラー: {str(e)}")
213 def commit(self) -> None:
214 """
215 現在のトランザクションをコミット(内部メソッド)
217 Raises:
218 DatabaseException: コミットに失敗した場合
219 """
220 if self._transaction is None:
221 raise DatabaseException("アクティブなトランザクションがありません")
222 self.commit_transaction(self._transaction)
224 def rollback(self) -> None:
225 """
226 現在のトランザクションをロールバック(内部メソッド)
228 Raises:
229 DatabaseException: ロールバックに失敗した場合
230 """
231 if self._transaction is None:
232 raise DatabaseException("アクティブなトランザクションがありません")
233 self.rollback_transaction(self._transaction)
235 def execute_query(
236 self,
237 query: str,
238 parameters: Optional[Dict[str, Any]] = None
239 ) -> List[Dict[str, Any]]:
240 """
241 Cypherクエリを実行(基底クラスメソッド)
243 Args:
244 query: Cypherクエリ文字列
245 parameters: クエリパラメータ
247 Returns:
248 クエリ結果のリスト
250 Raises:
251 DatabaseException: クエリ実行に失敗した場合
252 """
253 if self._driver is None:
254 raise DatabaseException("データベースに接続されていません")
256 try:
257 # トランザクション内の場合
258 if self._transaction is not None:
259 result = self._transaction.run(query, parameters or {})
260 return [dict(record) for record in result]
262 # トランザクション外の場合
263 with self._driver.session(database=self.database) as session:
264 result = session.run(query, parameters or {})
265 return [dict(record) for record in result]
267 except Neo4jError as e:
268 self.logger.error(f"Neo4j query error: {e}", exc_info=True)
269 raise DatabaseException(f"クエリ実行エラー: {str(e)}")
270 except Exception as e:
271 self.logger.error(f"Unexpected error during query execution: {e}", exc_info=True)
272 raise DatabaseException(f"予期しないエラー: {str(e)}")
274 def execute(
275 self,
276 query: str,
277 parameters: Optional[Dict[str, Any]] = None
278 ) -> List[Dict[str, Any]]:
279 """
280 Cypherクエリを実行(互換性メソッド)
282 Args:
283 query: Cypherクエリ文字列
284 parameters: クエリパラメータ
286 Returns:
287 クエリ結果のリスト
289 Raises:
290 DatabaseException: クエリ実行に失敗した場合
291 """
292 return self.execute_query(query, parameters)
294 @contextmanager
295 def transaction(self) -> Generator["Neo4jClient", None, None]:
296 """
297 トランザクションコンテキストマネージャー
299 Usage:
300 with client.transaction():
301 client.execute("CREATE (n:Person {name: $name})", {"name": "太郎"})
303 Yields:
304 Neo4jClient: 自身のインスタンス
305 """
306 self.begin_transaction()
307 try:
308 yield self
309 self.commit()
310 except Exception:
311 self.rollback()
312 raise
314 def clear_database(self) -> None:
315 """
316 データベースの全データを削除(テスト用)
318 Warning:
319 本番環境では使用しないこと
320 """
321 self.logger.warning("Clearing all data from database")
322 self.execute("MATCH (n) DETACH DELETE n")
324 def create_constraints(self) -> None:
325 """
326 制約とインデックスを作成
327 """
328 constraints = [
329 # Personノードのname属性にユニーク制約
330 "CREATE CONSTRAINT person_name_unique IF NOT EXISTS FOR (p:Person) REQUIRE p.name IS UNIQUE",
332 # Personノードのnameにインデックス
333 "CREATE INDEX person_name_index IF NOT EXISTS FOR (p:Person) ON (p.name)",
335 # is_decedentフラグにインデックス
336 "CREATE INDEX person_decedent_index IF NOT EXISTS FOR (p:Person) ON (p.is_decedent)",
338 # is_aliveフラグにインデックス
339 "CREATE INDEX person_alive_index IF NOT EXISTS FOR (p:Person) ON (p.is_alive)",
340 ]
342 for constraint in constraints:
343 try:
344 self.execute(constraint)
345 self.logger.debug(f"Constraint/Index created: {constraint[:50]}...")
346 except Exception as e:
347 # 制約が既に存在する場合は無視
348 self.logger.debug(f"Constraint/Index already exists or failed: {e}")
350 def __enter__(self) -> "Neo4jClient":
351 """コンテキストマネージャーのエントリー"""
352 self.connect()
353 return self
355 def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
356 """コンテキストマネージャーのイグジット"""
357 self.disconnect()