Can you copy/move data between columns in a DB migration file? - python

I have a SqlAlchemy/Flask application. In it, I have an existing model named MyModelA. This is what it looks like:
class MyModelA(db.Model):
a_id = db.Column(db.Integer, nullable=False, primary_key=True)
my_field1 = db.Column(db.String(1024), nullable=True)
Now, I am adding a child model MyModelB. This is what it looks like:
class MyModelB(db.Model):
b_id = db.Column(db.Integer, nullable=False, primary_key=True)
a_id = db.Column(db.Integer, db.ForeignKey(MyModelA.a_id), nullable=False)
my_field2 = db.Column(db.String(1024), nullable=True)
Then I run python manage.py migrate. This is what shows up in the migration file:
def upgrade():
op.create_table('my_model_b',
sa.Column('b_id', sa.Integer(), nullable=False),
sa.Column('a_id', sa.Integer(), nullable=False),
sa.Column('my_field2', sa.String(length=1024), nullable=True),
sa.ForeignKeyConstraint(['a_id'], [u'my_model_a.a_id'], ),
sa.PrimaryKeyConstraint('b_id')
)
def downgrade():
op.drop_table('my_table_b')
I want to edit this migration such that it for every instance of MyModelA, a child record of instance MyModelB should be created with MyModelB.my_field2 set to MyModelA.my_field1. How can I do it?
Please show the code for upgrade and downgrade.

Edit:
You can do something like this for the one time migration:
db.engine.execute("INSERT INTO model_b (a_id) select a_id from model_a");
of if you really want sqlalschemy code:
for model in db.query(ModelA).all()
db.session.add(ModelB(a_id=model.id))
db.session.commit()
Previous answer:
What you are describing is not something you typically do in migrations. Migrations change/create the structure of your database. If you need it to happen every time a new MyModelA is created, this sounds more like events: http://docs.sqlalchemy.org/en/latest/orm/events.html#session-events
class MyModelA(db.Model):
...
#sqlalchemy.event.listens_for(SignallingSession, 'before_flush')
def insert_model_b(session, transaction, instances):
for instance in session.new:
if isinstance(instance, MyModelA):
model_b = MyModelB(a=instance)
session.add(model_b)
Also, your schema needs to show that relationship (not just the foreign key) so you can assign the yet uninserted model_a to model_b.a:
class MyModelB(db.Model):
b_id = db.Column(db.Integer, nullable=False, primary_key=True)
a_id = db.Column(db.Integer, db.ForeignKey(MyModelA.a_id), nullable=False)
a = relationship("MyModelA")
my_field2 = db.Column(db.String(1024), nullable=True)
Full code example:
import sqlalchemy
from sqlalchemy.orm import relationship
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.sqlalchemy import SignallingSession
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
class MyModelA(db.Model):
__tablename__ = 'model_a'
a_id = db.Column(db.Integer, nullable=False, primary_key=True)
my_field1 = db.Column(db.String(1024), nullable=True)
class MyModelB(db.Model):
__tablename__ = 'model_b'
b_id = db.Column(db.Integer, nullable=False, primary_key=True)
a_id = db.Column(db.Integer, db.ForeignKey(MyModelA.a_id), nullable=False)
a = relationship(MyModelA)
my_field2 = db.Column(db.String(1024), nullable=True)
#sqlalchemy.event.listens_for(SignallingSession, 'before_flush')
def insert_model_b(session, transaction, instances):
for instance in session.new:
if isinstance(instance, MyModelA):
model_b = MyModelB(a=instance)
session.add(model_b)
db.create_all()
model_a = MyModelA()
db.session.add(model_a)
db.session.commit()

Related

Adding multiple foreign keys from same model (FastAPI and SqlAlechemy)

What I am trying to do is have 2 foreign keys from User table inside Ban table here is how I did it:
class Ban(Base):
__tablename__ = "ban"
ban_id = Column(Integer, primary_key=True, index=True)
poll_owner_id = Column(Integer)
banned_by = Column(String , ForeignKey('user.username', ondelete='CASCADE', ), unique=True)
user_id = Column(Integer, ForeignKey('user.user_id', ondelete='CASCADE', ))
updated_at = Column(DateTime)
create_at = Column(DateTime)
ban_to_user = relationship("User", back_populates='user_to_ban', cascade='all, delete')
and User table:
class User(Base):
__tablename__ = "user"
user_id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True)
email = Column(String)
create_at = Column(DateTime)
updated_at = Column(DateTime)
user_to_ban = relationship("Ban", back_populates='ban_to_user', cascade='all, delete')
When I try to run a query to fetch all users like this:
#router.get('/all')
async def get_all_users(db:Session = Depends(get_db)):
return db.query(models.User).all()
I get this error:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'mapped class User->user'. Origina
l exception was: Could not determine join condition between parent/child tables on relationship User.user_to_ban - there are multiple foreign key paths linking the tables. Specify the 'foreign_keys' argument, providing a list of those columns which should be counted as containing a foreign key reference to the parent table.
I did the relationship between them as you can see but it states that there is problem between them. If needed I can show you how I did migration for my db using alembic if that is possible cause or is there a cleaner and better way to do this. Thanks in advance
You can have several foreign keys to a single table, like in your case for banned user and banned_by user.
You just need to disambiguate, which ForeignKey for which relationship (docs):
class Ban(Base):
__tablename__ = "ban"
id = Column(Integer, primary_key=True)
banned_user_id = Column(Integer, ForeignKey("user.id")) # for banned_user relationship
banned_by_user_id = Column(Integer, ForeignKey("user.id")) # for banned_by relationship
banned_user = relationship("User", foreign_keys=[banned_user_id], back_populates="bans")
banned_by = relationship("User", foreign_keys=[banned_by_user_id])
Full demo:
from sqlalchemy import (
Column,
ForeignKey,
Integer,
String,
create_engine,
select,
)
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True)
bans = relationship(
"Ban",
back_populates="banned_user",
foreign_keys="Ban.banned_user_id",
)
class Ban(Base):
__tablename__ = "ban"
id = Column(Integer, primary_key=True)
banned_user_id = Column(Integer, ForeignKey("user.id"))
banned_by_user_id = Column(Integer, ForeignKey("user.id"))
banned_user = relationship(
"User", foreign_keys=[banned_user_id], back_populates="bans"
)
banned_by = relationship("User", foreign_keys=[banned_by_user_id])
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
spongebob = User(username="spongebob")
patrick = User(username="patrickstarr")
spongebob_bans_patrick = Ban(banned_by=spongebob, banned_user=patrick)
with Session(engine) as session:
session.add_all(
[
spongebob,
patrick,
spongebob_bans_patrick,
]
)
session.commit()
with Session(engine) as session:
result = session.scalars(select(Ban)).first()
print(
"User:",
result.banned_user.username,
"was banned by User:",
result.banned_by.username,
)
# User: patrickstarr was banned by User: spongebob

How can I have two tables A and B where B has all columns of A?

I am currently building a CMS. I want to have a page table and a page_revision table, where page_revision has all columns of page + rev_id + rev_parent_id.
MVCE
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Page(db.Model):
__tablename__ = 'pages'
page_id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text(), default='', nullable=False)
class PageRevision(Page):
__tablename__ = 'pages_revisions'
rev_id = db.Column(db.Integer, primary_key=True)
rev_parent_id = db.Column(db.Integer, nullable=True)
The error I get when I try flask db migrate:
sqlalchemy.exc.NoForeignKeysError: Can't find any foreign key relationships between 'pages' and 'pages_revisions'.
I was not sure what should happen. What I'm trying to do is to copy the structure, but I don't want inheritance in the OO sense.
Is there a way to copy all columns (make page_id NOT the primary key) without simply copy-and-paste?
This feels like a dirty hack and I'm not quite sure if it even works as expected but it might give some inspiration ;)
The idea is to create the PageRevision class dynamically by copying the fields of Page.
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Page(db.Model):
__tablename__ = 'pages'
page_id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text(), default='', nullable=False)
def columns_of_table(table_type):
for col in Page.__table__.columns:
yield col.key, db.Column(col.type, default=col.default, nullable=col.nullable, primary_key=False)
PageRevision = type('PageRevision', (db.Model, ), {
'rev_id': db.Column(db.Integer, primary_key=True),
'rev_parent_id': db.Column(db.Integer, nullable=True),
**{key: col for key, col in columns_of_table(Page)}
})

cannot get one to many relationship working in (Flask-) SQLAlchemy

I have several classes:
import uuid
from app import db, create_app
from sqlalchemy.sql import func
from sqlalchemy.dialects.postgresql import UUID, ARRAY, JSONB
class Ticket(db.Model):
__tablename__ = 'tickets'
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
time = db.Column(db.DateTime, server_default=func.now(), index=True)
proposed_names = db.Column(ARRAY(db.String))
measurements = db.relationship('TempMeasurement', back_populates='ticket')
class BaseMeasurement(object):
id = db.Column(db.Integer, primary_key=True)
#declared_attr
def type_id(self):
return db.Column(db.Integer, db.ForeignKey('optical_data_types.id'))
#declared_attr
def type(self):
return db.relationship('OpticalDataType')
#declared_attr
def operator_id(self):
return db.Column(db.Integer, db.ForeignKey('operators.id'))
#declared_attr
def operator(self):
return db.relationship('Operator')
#declared_attr
def item_id(self):
return db.Column(db.String, db.ForeignKey('items.serial'))
#declared_attr
def item(self):
return db.relationship('Item')
time = db.Column(db.DateTime, index=True)
instrument = db.Column(db.String)
instrument_sn = db.Column(db.String)
data = db.Column(JSONB)
class TempMeasurement(db.Model, BaseMeasurement):
__tablename__ = 'ticket_data'
id = db.Column(db.Integer, primary_key=True)
ticket_id = db.Column(UUID(as_uuid=True), db.ForeignKey('tickets.id'), index=True)
ticket = db.relationship('Ticket', back_populates='measurements')
original_paths = db.Column(ARRAY(db.String))
What I want/expect is that I can create a Ticket with several child TempMeasurements and commit this to the database. Something like:
app = create_app()
with app.app_context():
ticket = Ticket()
ticket.measurements = [TempMeasurement(...)]
db.session.add(ticket) # <-- error on this line
db.session.commit()
However, I get an obscure error deep in SQLAlchemy:
AttributeError: 'str' object has no attribute '_sa_instance_state'
with a full trace here.
I thought that it might be because the UUID ticket_id column has as_uuid, so I made it simply UUID (implicitly a str), but this did not solve my issue.
The error is too deep in SQLAlchemy for me to understand -- can anyone help?

sqlalchemy Error creating backref on relationship

I have two very simple models. In my Post model there are supposed to be two relationships into the User table. One is for the owner of the post and one is for the last editor of the post. They can be different values, but both refer to the same User table.
My models are set up like this
class Post(Base):
last_editor_id = Column(BigInteger, ForeignKey('users.id'), nullable=True)
last_editor = relationship('User', backref='posts', foreign_keys=[last_editor_id])
owner_id = Column(BigInteger, ForeignKey('users.id'), nullable=False, index=True)
owner = relationship('User', backref='posts', foreign_keys=[owner_id])
class User(Base):
'''This represents a user on the site'''
__tablename__ = 'users'
id = Column(BigInteger, primary_key=True, unique=True)
name = Column(BigInteger, nullable=False)
When I attempt to create these models though, I get the following error
sqlalchemy.exc.ArgumentError: Error creating backref 'posts' on relationship 'Post.owner': property of that name exists on mapper 'Mapper|User|users'
How do I correct this so that I can maintain both forgeign keys in the Post model?
The error is telling you that you've used post as a name more then once for your backrefs, all you need to do is give the backref's unique names. Here's a complete example-- I've added a id primary key to the Post class, and also some __repr__s so we get some readable output.
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, BigInteger, ForeignKey, Integer
from sqlalchemy.orm import relationship, sessionmaker
Base = declarative_base()
engine = create_engine('sqlite://') ## In Memory.
Session = sessionmaker()
Session.configure(bind=engine)
session = Session()
class Post(Base):
__tablename__ = 'post'
id = Column(Integer, primary_key=True)
last_editor_id = Column(BigInteger, ForeignKey('users.id'), nullable=True)
last_editor = relationship('User', backref='editor_posts', foreign_keys=[last_editor_id])
owner_id = Column(BigInteger, ForeignKey('users.id'), nullable=False, index=True)
owner = relationship('User', backref='owner_posts', foreign_keys=[owner_id])
def __repr__(self):
return '<Post: {}>'.format(self.id)
class User(Base):
'''This represents a user on the site'''
__tablename__ = 'users'
id = Column(BigInteger, primary_key=True, unique=True)
name = Column(BigInteger, nullable=False)
def __repr__(self):
return '<User: {}>'.format(self.name)
Base.metadata.create_all(engine)
bob = User(name='Bob', id=1)
alice = User(name='Alice', id=2)
post = Post(owner=alice, last_editor=bob, id=1)
session.add(post)
session.commit()
bob = session.query(User).get(1)
print bob
# <User: Bob>
print bob.editor_posts
# [<Post: 1>]
print bob.owner_posts
# []
post = session.query(Post).get(1)
print post.owner
# <User: Alice>
print post.last_editor
# <User: Bob>
Now when you query a user, you can ask that object user.owner_posts or user.editor_posts.
In general it's a naming Problem of the backref.
Since 1:n relationships are sometimes a bit confusing, I set the relationship attribute
always on the singular site, to avoid confusion.
then the backref name is always singular. and the relationship attribute is always in the Class where the foreignkey is referencing to.
Now to my suggestion for the fixed code:
class Post(Base):
last_editor_id = Column(BigInteger, ForeignKey('users.id'), nullable=True)
owner_id = Column(BigInteger, ForeignKey('users.id'), nullable=False, index=True)
class User(Base):
'''This represents a user on the site'''
__tablename__ = 'users'
id = Column(BigInteger, primary_key=True, unique=True)
name = Column(BigInteger, nullable=False)
owned_posts = relationship('Post', backref='owner')
edited_posts = relationship('Post', backref='last_editor')
Now you can get all the owned posts of a User with User.owned_posts and all owners of a post with Post.owner. Same with the last_edited attribute.
For additional info you could read the docs how to set up relationships

flask-migrate wants to drop my indeces

I've got a Flask app with the following models:
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
city_id = db.Column(db.Integer, db.ForeignKey('cities.id'))
class City(db.Model):
__tablename__ = 'cities'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
user_ids = db.relationship('User', backref='city', lazy='dynamic')
I've run a migration to specify my indices and foreign key constraints:
def upgrade():
op.create_foreign_key('fk_user_city', "users", "cities", ["city_id"], ["id"])
op.create_index('city_idx', 'users', ['city_id'])
However, any time I create another new migration Alembic seems to want to drop my indexes.
Is there a way to freeze Alembic's autogeneration at the current DB/Model schema?
Check this page. You will need to change env.py under migrations folder.
EnvironmentContext.configure.include_object
or
EnvironmentContext.configure.include_schemas
should be what you are looking for.

Categories

Resources