Tengo 4 bases de datos diferentes, una para cada uno de mis clientes (clínicas médicas), las cuales tienen exactamente la misma estructura.
En mi aplicación, tengo modelos como Patient , Doctor , Appointment , etc.
Tomemos uno de ellos como ejemplo:
class Patient(db.Model): __tablename__ = "patients" id = Column(Integer, primary_key=True) first_name = Column(String, index=True) last_name = Column(String, index=True) date_of_birth = Column(Date, index=True)Descubrí que con la ayuda de enlaces puedo crear diferentes bases de datos y asociar cada modelo a un enlace diferente. Así que tengo esta configuración:
app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://user:pass@localhost/main' app.config['SQLALCHEMY_BINDS'] = { 'clinic1':'mysql://user:pass@localhost/clinic1', 'clinic2':'mysql://user:pass@localhost/clinic2', 'clinic3':'mysql://user:pass@localhost/clinic3', 'clinic4':'mysql://user:pass@localhost/clinic4' }Ahora estoy tratando de lograr dos cosas:
db.create_all() cree la tabla de patients en las 4 bases de datos (clinic1->clinic4)Patient.query.filter().count() se ejecute en la base de datos de enlace elegidaIdealmente, se comportaría así:
with DbContext(bind='client1'): patients_count = Patient.query.filter().count() print(patients_count) # outside of the `with` context we are back to the default bindSin embargo, haciendo esto:
patients_count = Patient.query.filter().count() sin especificar un enlace, generará un error (ya que la tabla de patients no existe en el enlace predeterminado)
¡Cualquier ejemplo de código que pueda guiar cómo se puede hacer esto sería muy apreciado!
PD: Puede ser que sugiera no usar diferentes bases de datos y, en su lugar, usar una con diferentes columnas/tablas, pero siga mi ejemplo e intente explicar cómo se puede hacer esto usando este patrón de múltiples bases de datos idénticas.
¡Gracias!
Observación: db.create_all() llama a self.get_tables_for_bind() .
Solución: anule SQLAlchemy get_tables_for_bind() para admitir '__all__' .
class MySQLAlchemy(SQLAlchemy): def get_tables_for_bind(self, bind=None): result = [] for table in self.Model.metadata.tables.values(): # if table.info.get('bind_key') == bind: if table.info.get('bind_key') == bind or (bind is not None and table.info.get('bind_key') == '__all__'): result.append(table) return resultUso:
# db = SQLAlchemy(app) # Replace this db = MySQLAlchemy(app) # with this db.create_all() Observación: SignallingSession get_bind() es responsable de determinar el enlace.
Solución:
SignallingSession get_bind() para obtener la clave de vinculación de algún contexto.SQLAlchemy create_session() para usar nuestra clase de sesión personalizada.db para accesibilidad.'__all__' como clave de vinculación, anulando SQLAlchemy get_binds() para restaurar el motor predeterminado. class MySignallingSession(SignallingSession): def __init__(self, db, *args, **kwargs): super().__init__(db, *args, **kwargs) self.db = db def get_bind(self, mapper=None, clause=None): if mapper is not None: info = getattr(mapper.persist_selectable, 'info', {}) if info.get('bind_key') == '__all__': info['bind_key'] = self.db.context_bind_key try: return super().get_bind(mapper=mapper, clause=clause) finally: info['bind_key'] = '__all__' return super().get_bind(mapper=mapper, clause=clause) class MySQLAlchemy(SQLAlchemy): context_bind_key = None @contextmanager def context(self, bind=None): _context_bind_key = self.context_bind_key try: self.context_bind_key = bind yield finally: self.context_bind_key = _context_bind_key def create_session(self, options): return orm.sessionmaker(class_=MySignallingSession, db=self, **options) def get_binds(self, app=None): binds = super().get_binds(app=app) # Restore default engine for table.info.get('bind_key') == '__all__' app = self.get_app(app) engine = self.get_engine(app, None) tables = self.get_tables_for_bind('__all__') binds.update(dict((table, engine) for table in tables)) return binds def get_tables_for_bind(self, bind=None): result = [] for table in self.Model.metadata.tables.values(): if table.info.get('bind_key') == bind or (bind is not None and table.info.get('bind_key') == '__all__'): result.append(table) return resultUso:
class Patient(db.Model): __tablename__ = "patients" __bind_key__ = "__all__" # Add thisCaso de prueba:
with db.context(bind='clinic1'): db.session.add(Patient()) db.session.flush() # Flush in 'clinic1' with db.context(bind='clinic2'): patients_count = Patient.query.filter().count() print(patients_count) # 0 in 'clinic2' patients_count = Patient.query.filter().count() print(patients_count) # 1 in 'clinic1' Tienes que especificar el schema .
Limitaciones:
MySQLdb._exceptions.OperationalError: (1205, 'Se excedió el tiempo de espera de bloqueo; intente reiniciar la transacción')
Uso:
# app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://user:pass@localhost/main' class PatientType(db.Model): __tablename__ = "patient_types" __table_args__ = {"schema": "main"} # Add this, based on database name id = Column(Integer, primary_key=True) # ... class Patient(db.Model): __tablename__ = "patients" __bind_key__ = "__all__" id = Column(Integer, primary_key=True) # ... # patient_type_id = Column(Integer, ForeignKey("patient_types.id")) # Replace this patient_type_id = Column(Integer, ForeignKey("main.patient_types.id")) # with this patient_type = relationship("PatientType")Caso de prueba:
patient_type = PatientType.query.first() if not patient_type: patient_type = PatientType() db.session.add(patient_type) db.session.commit() # Commit to reference from other binds with db.context(bind='clinic1'): db.session.add(Patient(patient_type=patient_type)) db.session.flush() # Flush in 'clinic1' with db.context(bind='clinic2'): patients_count = Patient.query.filter().count() print(patients_count) # 0 in 'clinic2' patients_count = Patient.query.filter().count() print(patients_count) # 1 in 'clinic1'