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

1"""Neo4jクライアント 

2 

3Neo4jデータベースへの接続と操作を管理するクライアント。 

4""" 

5from typing import Any, Dict, List, Optional, Generator 

6from contextlib import contextmanager 

7import logging 

8 

9from neo4j import GraphDatabase, Driver, Session, Transaction 

10from neo4j.exceptions import ServiceUnavailable, AuthError, Neo4jError 

11 

12from .base import DatabaseClient 

13from ..utils.exceptions import DatabaseException 

14from ..utils.config import Neo4jSettings 

15 

16 

17class Neo4jClient(DatabaseClient): 

18 """ 

19 Neo4jデータベースクライアント 

20 

21 接続管理、トランザクション管理、クエリ実行を提供する。 

22 """ 

23 

24 def __init__(self, settings: Optional[Neo4jSettings] = None) -> None: 

25 """ 

26 初期化 

27 

28 Args: 

29 settings: Neo4j接続設定(Noneの場合は環境変数から取得) 

30 """ 

31 self.logger = logging.getLogger(__name__) 

32 

33 if settings is None: 

34 try: 

35 settings = Neo4jSettings() # type: ignore[call-arg] 

36 except Exception: 

37 # テスト環境等でパスワードが設定されていない場合のフォールバック 

38 raise DatabaseException("Neo4j設定の初期化に失敗しました。NEO4J_PASSWORD環境変数が設定されているか確認してください。") 

39 

40 self.uri = settings.uri 

41 self.user = settings.user 

42 self.password = settings.password 

43 self.database = settings.database 

44 

45 self._driver: Optional[Driver] = None 

46 self._session: Optional[Session] = None 

47 self._transaction: Optional[Transaction] = None 

48 

49 self.logger.info(f"Neo4jClient initialized for {self.uri}") 

50 

51 def connect(self) -> None: 

52 """ 

53 データベースに接続 

54 

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 ) 

64 

65 # 接続テスト 

66 self._driver.verify_connectivity() 

67 

68 self.logger.info("Successfully connected to Neo4j") 

69 

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)}") 

79 

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 

89 

90 # セッションを閉じる 

91 if self._session is not None: 

92 self._session.close() 

93 self._session = None 

94 

95 # ドライバーを閉じる 

96 if self._driver is not None: 

97 self._driver.close() 

98 self._driver = None 

99 

100 self.logger.info("Disconnected from Neo4j") 

101 

102 except Exception as e: 

103 self.logger.error(f"Error during disconnect: {e}", exc_info=True) 

104 

105 def is_connected(self) -> bool: 

106 """ 

107 接続状態を確認 

108 

109 Returns: 

110 接続中の場合True 

111 """ 

112 if self._driver is None: 

113 return False 

114 

115 try: 

116 self._driver.verify_connectivity() 

117 return True 

118 except Exception: 

119 return False 

120 

121 def health_check(self) -> bool: 

122 """ 

123 ヘルスチェック 

124 

125 Returns: 

126 データベースが正常に動作している場合True 

127 """ 

128 if self._driver is None: 

129 return False 

130 

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 

141 

142 def begin_transaction(self) -> Transaction: 

143 """ 

144 トランザクションを開始 

145 

146 Returns: 

147 トランザクションオブジェクト 

148 

149 Raises: 

150 DatabaseException: トランザクション開始に失敗した場合 

151 """ 

152 if self._driver is None: 

153 raise DatabaseException("データベースに接続されていません") 

154 

155 try: 

156 if self._session is None: 

157 self._session = self._driver.session(database=self.database) 

158 

159 self._transaction = self._session.begin_transaction() 

160 self.logger.debug("Transaction started") 

161 return self._transaction 

162 

163 except Exception as e: 

164 self.logger.error(f"Failed to begin transaction: {e}", exc_info=True) 

165 raise DatabaseException(f"トランザクション開始エラー: {str(e)}") 

166 

167 def commit_transaction(self, transaction: Any) -> None: 

168 """ 

169 トランザクションをコミット 

170 

171 Args: 

172 transaction: トランザクションオブジェクト 

173 

174 Raises: 

175 DatabaseException: コミットに失敗した場合 

176 """ 

177 if transaction is None: 

178 raise DatabaseException("トランザクションがNullです") 

179 

180 try: 

181 transaction.commit() 

182 if self._transaction == transaction: 

183 self._transaction = None 

184 self.logger.debug("Transaction committed") 

185 

186 except Exception as e: 

187 self.logger.error(f"Failed to commit transaction: {e}", exc_info=True) 

188 raise DatabaseException(f"トランザクションコミットエラー: {str(e)}") 

189 

190 def rollback_transaction(self, transaction: Any) -> None: 

191 """ 

192 トランザクションをロールバック 

193 

194 Args: 

195 transaction: トランザクションオブジェクト 

196 

197 Raises: 

198 DatabaseException: ロールバックに失敗した場合 

199 """ 

200 if transaction is None: 

201 raise DatabaseException("トランザクションがNullです") 

202 

203 try: 

204 transaction.rollback() 

205 if self._transaction == transaction: 

206 self._transaction = None 

207 self.logger.debug("Transaction rolled back") 

208 

209 except Exception as e: 

210 self.logger.error(f"Failed to rollback transaction: {e}", exc_info=True) 

211 raise DatabaseException(f"トランザクションロールバックエラー: {str(e)}") 

212 

213 def commit(self) -> None: 

214 """ 

215 現在のトランザクションをコミット(内部メソッド) 

216 

217 Raises: 

218 DatabaseException: コミットに失敗した場合 

219 """ 

220 if self._transaction is None: 

221 raise DatabaseException("アクティブなトランザクションがありません") 

222 self.commit_transaction(self._transaction) 

223 

224 def rollback(self) -> None: 

225 """ 

226 現在のトランザクションをロールバック(内部メソッド) 

227 

228 Raises: 

229 DatabaseException: ロールバックに失敗した場合 

230 """ 

231 if self._transaction is None: 

232 raise DatabaseException("アクティブなトランザクションがありません") 

233 self.rollback_transaction(self._transaction) 

234 

235 def execute_query( 

236 self, 

237 query: str, 

238 parameters: Optional[Dict[str, Any]] = None 

239 ) -> List[Dict[str, Any]]: 

240 """ 

241 Cypherクエリを実行(基底クラスメソッド) 

242 

243 Args: 

244 query: Cypherクエリ文字列 

245 parameters: クエリパラメータ 

246 

247 Returns: 

248 クエリ結果のリスト 

249 

250 Raises: 

251 DatabaseException: クエリ実行に失敗した場合 

252 """ 

253 if self._driver is None: 

254 raise DatabaseException("データベースに接続されていません") 

255 

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] 

261 

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] 

266 

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)}") 

273 

274 def execute( 

275 self, 

276 query: str, 

277 parameters: Optional[Dict[str, Any]] = None 

278 ) -> List[Dict[str, Any]]: 

279 """ 

280 Cypherクエリを実行(互換性メソッド) 

281 

282 Args: 

283 query: Cypherクエリ文字列 

284 parameters: クエリパラメータ 

285 

286 Returns: 

287 クエリ結果のリスト 

288 

289 Raises: 

290 DatabaseException: クエリ実行に失敗した場合 

291 """ 

292 return self.execute_query(query, parameters) 

293 

294 @contextmanager 

295 def transaction(self) -> Generator["Neo4jClient", None, None]: 

296 """ 

297 トランザクションコンテキストマネージャー 

298 

299 Usage: 

300 with client.transaction(): 

301 client.execute("CREATE (n:Person {name: $name})", {"name": "太郎"}) 

302 

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 

313 

314 def clear_database(self) -> None: 

315 """ 

316 データベースの全データを削除(テスト用) 

317 

318 Warning: 

319 本番環境では使用しないこと 

320 """ 

321 self.logger.warning("Clearing all data from database") 

322 self.execute("MATCH (n) DETACH DELETE n") 

323 

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", 

331 

332 # Personノードのnameにインデックス 

333 "CREATE INDEX person_name_index IF NOT EXISTS FOR (p:Person) ON (p.name)", 

334 

335 # is_decedentフラグにインデックス 

336 "CREATE INDEX person_decedent_index IF NOT EXISTS FOR (p:Person) ON (p.is_decedent)", 

337 

338 # is_aliveフラグにインデックス 

339 "CREATE INDEX person_alive_index IF NOT EXISTS FOR (p:Person) ON (p.is_alive)", 

340 ] 

341 

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}") 

349 

350 def __enter__(self) -> "Neo4jClient": 

351 """コンテキストマネージャーのエントリー""" 

352 self.connect() 

353 return self 

354 

355 def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 

356 """コンテキストマネージャーのイグジット""" 

357 self.disconnect()