How do I create a Django migration for my ManyToMany relation that includes an on-delete cascade? - python-3.x

I'm using PostGres 10, Python 3.9, and Django 3.2. I have set up this model with the accompanying many-to-many relationship ...
class Account(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
...
crypto_currencies = models.ManyToManyField(CryptoCurrency)
After generating and running Django migrations, the following table was created ...
\d cbapp_account_crypto_currencies;
Table "public.cbapp_account_crypto_currencies"
Column | Type | Modifiers
-------------------+---------+------------------------------------------------------------------------------
id | integer | not null default nextval('cbapp_account_crypto_currencies_id_seq'::regclass)
account_id | uuid | not null
cryptocurrency_id | uuid | not null
Indexes:
"cbapp_account_crypto_currencies_pkey" PRIMARY KEY, btree (id)
"cbapp_account_crypto_cur_account_id_cryptocurrenc_38c41c43_uniq" UNIQUE CONSTRAINT, btree (account_id, cryptocurrency_id)
"cbapp_account_crypto_currencies_account_id_611c9b45" btree (account_id)
"cbapp_account_crypto_currencies_cryptocurrency_id_685fb811" btree (cryptocurrency_id)
Foreign-key constraints:
"cbapp_account_crypto_account_id_611c9b45_fk_cbapp_acc" FOREIGN KEY (account_id) REFERENCES cbapp_account(id) DEFERRABLE INITIALLY DEFERRED
"cbapp_account_crypto_cryptocurrency_id_685fb811_fk_cbapp_cry" FOREIGN KEY (cryptocurrency_id) REFERENCES cbapp_cryptocurrency(id) DEFERRABLE INITIALLY DEFERRED
How do I alter my field relation, or generate a migration, such that the cascade relationship is ON-DELETE CASCADE? That is, When I delete an account, I would like accompanying records in this table to also be deleted.

Had a closer look on this. I tried to replicate your models and I also see that the intermediary table has no cascade. I have no answer on your main question on how to add the cascade, but it seems that django does the cascade behavior which already supports this:
When I delete an account, I would like accompanying records in this table to also be deleted.
To demonstrate:
a = Account.objects.create(name='test')
c1 = CryptoCurrency.objects.create(name='c1')
c2 = CryptoCurrency.objects.create(name='c2')
c3 = CryptoCurrency.objects.create(name='c3')
a.crypto_currencies.set([c1, c2, c3])
If you do:
a.delete()
Django runs the following SQL which simulates the cascade on the intermediary table:
[
{
'sql': 'DELETE FROM "myapp_account_crypto_currencies" WHERE "myapp_account_crypto_currencies"."account_id" IN (3)', 'time': '0.002'
},
{
'sql': 'DELETE FROM "myapp_account" WHERE "myapp_account"."id" IN (3)', 'time': '0.001'
}
]
I can't find in the documentation why it is done this way though. Even adding a custom intermediary like this results in the same behavior:
class Account(models.Model):
name = models.CharField(max_length=100)
crypto_currencies = models.ManyToManyField(CryptoCurrency, through='myapp.AccountCryptocurrencies')
class AccountCryptocurrencies(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE)
cryptocurrency = models.ForeignKey(CryptoCurrency, on_delete=models.CASCADE)

When you use a ManyToManyField, Django creates a intermediary table for you, in this case named cbapp_account_crypto_currencies. What you want to do in the future is to always explicitly create the intermediary model, AccountCryptoCurrencies, then set the through attribute of the ManyToManyField. This will allow you to add more fields in the future to the intermediary model. See more here: https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.ManyToManyField.through.
What you will now need to do is so create this intermediary table:
class AccountCryptoCurrencies(models.Model):
account = models.ForeignKey(Account)
cryptocurrency = models.ForeignKey(CryptoCurrency)
class Meta:
db_table = 'cbapp_account_crypto_currencies'
class Account(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
...
crypto_currencies = models.ManyToManyField(CryptoCurrency, through=AccountCryptoCurrencies)
You are now need to generate a migration, but do not apply it yet! Modify the migration by wrapping it in a SeparateDatabaseAndState. I havent created your migration file because I dont have the full model, but you can see here for how to do it: How to add through option to existing ManyToManyField with migrations and data in django
Now you can apply the migration and you should now have an explicit intermediary table without losing data. You can also now add additional fields to the intermediary table and change the existing fields. You can add the on_delete=models.CASCADE to the account field and migrate the change.

Related

How to add attributes to database columns

Im currently working on creating correct database columns for my database. I have created two tables and used alter:
CREATE TABLE stores (
id SERIAL PRIMARY KEY,
store_name TEXT
-- add more fields if needed
);
CREATE TABLE products (
id SERIAL,
store_id INTEGER NOT NULL,
title TEXT,
image TEXT,
url TEXT UNIQUE,
added_date timestamp without time zone NOT NULL DEFAULT NOW(),
PRIMARY KEY(id, store_id)
);
ALTER TABLE products
ADD CONSTRAINT "FK_products_stores" FOREIGN KEY ("store_id")
REFERENCES stores (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE RESTRICT;
Now I am trying to use it together with PeeWee and I have managed to do a small step which is:
class Stores(Model):
id = IntegerField(column_name='id')
store_id = TextField(column_name='store_name')
class Products(Model):
id = IntegerField(column_name='id')
store_id = IntegerField(column_name='store_id')
title = TextField(column_name='title')
url = TextField(column_name='url')
image = TextField(column_name='image')
However my problem is that I have used:
ALTER TABLE products
ADD CONSTRAINT "FK_products_stores" FOREIGN KEY ("store_id")
REFERENCES stores (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE RESTRICT;
which means that I do have a Foreign key and I am quite not sure how I can apply to use Foreign key together with PeeWee. I wonder how can I do that?
You need to add a ForeignKeyField to Products and remove store_id
class Products(Model):
id = IntegerField(column_name='id')
title = TextField(column_name='title')
url = TextField(column_name='url')
image = TextField(column_name='image')
store = ForeignKeyField(Stores, backref='products')

Multiple foreign keys referencing same table column in SQLAlchemy

I am creating a rating system. Rating is a table which contains individual ratings as rows. Each rating has a "rater" and a "ratee". These two columns reference a different table, "User", by means of a foreign key. However, they both reference the same user.id column. Code:
class Rating(db.Model):
id = db.Column(db.Integer, primary_key=True)
rater_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
ratee_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
And here is how they are represented from within the User class (table):
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), unique=True, nullable=False)
ratesOfOthers = db.relationship('Rating', backref='rater', lazy=True)
ratingsByOthers = db.relationship('Rating', backref='ratee', lazy=True)
Now, when I try to use this relationship, I am presented with the following error:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.ratesOfOthers - 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 have tried using the foreign_keys argument from inside the User class, but that did nothing. Any help would be appreciated.
The assumption is that in your scenario a user can rate other users, and they (themselves) can be rated. Basically, there is one table called User referencing other users within an application.
A relationship in which instances of a class are linked to other instances of the same class is called self-referential relationship, and that is exactly what you have here.
Here is a diagram that represents this self-referential many-to-many relationship that keeps track of ratings:
The Ratings table is the association table of the relationship. The foreign keys in this table are pointing at entries in the User table since it is linking users to users.
To add this table to your database, this is how you can go about it:
ratings = db.Table('ratings'
db.Column('my_ratings_id', db.Integer, db.ForeignKey('user.id'))
db.Column('other_people_rating_id', db.Integet, db.ForeignKey('user.id'))
)
This is an auxiliary table (directly-translated as seen above) that has no data other than foreign keys. It is, therefore, created without an associated model class.
To declare the many-to-many relationship in the User table, add this:
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(32), unique=True, nullable=False)
def __repr__(self):
return f'{self.username}'
my_ratings = db.relationship(
'User',
secondary=ratings,
primaryjoin=(ratings.c.my_ratings_id == id),
secondaryjoin=(ratings.c.other_people_rating_id == id),
backref = db.backref('other_people_rating', lazy='dynamic'), lazy='dynamic'
)
I am defining the relationship as seen from the left side user with the name my_ratings because when you query this relationship from the left side, you will get a list of all those on the right side. Visually, this is what I mean:
Examining all the arguments of the db.relationship() call, you will see that:
User is the right side entity of the relationship.
secondary configures the ratings association table
primaryjoin indicates the condition that links the left side entity with the association table. The user id should match my_ratings_id
secondaryjoin indicates the condition that links the right side entity with the association table. Again, other_people_rating_id should match the user id
backref defines how this relationship will be accessed from the right side entity. From the left side, the relationship is named my_ratings, so from the right side, I decided to name it other_people_rating to represent all the left side users that are linked to the target user in the right side.
The dynamic mode is used to set up the query to not run until specifically requested.
The second lazy parameter applies to the left side query instead of the right side.

Deletion of a row from an association table

I am working on an app using python3 and SqlAlchemy for SQLite3 database management. I have some tables that have a Many to Many relationship. I've created an association table to handle this relationship.
Class Machine(Base):
__tablename__ 'machine'
machine_ID = Column(Integer, primary_key=True)
etc...
Class Options(Base):
__tableName__ 'options'
options_ID = Column(Integer, primary_key=True)
etc...
The association table
Machine_Options = table('machine_options', Base.metadata,
Column('machine_FK', Integer, ForeignKey('machine.machine_ID'),
primary_key=True),
Column('options_FK',Integer, ForeignKey('options.options_ID'),
primary_key=True))
All the items for the Machine and Options are inserted independently. When I want to associate a machine with an option I use an append query which works very well.
My problem is when I want to break this association between a machine and an option. I have tried a direct row deletion from the association table using a FILTER() clause on the machine_FK and the options_FK but SqlAlchemy gives me an error informing me that 'Machine_Options' table has no field 'machine_FK'.
I have tried to remove the row from 'Machine_Options' indirectly using joins with the machine and options table but received another error that I can not delete or update using joins.
I am looking for the code to only delete a row from the association table without affecting the original machine or options table.
So far my internet search has been fruitless.
The answer to my problem is to use myparent.children.remove(somechild)
The association is made using machine.children.append(option)
Using the same code as the 'append' and substituting 'remove' unmakes the association
The code:
def removeOption(machineKey, OptionKey):
session = connectToDatabase()
machineData = session.query(Machine).filter(Machine.machine_ID == machineKey).one()
optionData = session.query(Options).filter(Options. options_ID == OptionKey).one()
machineData.children.remove(optionData)
session.add(machineData)
session.commit()
session.close()

Many to Many relationship Flask sqlalchemy and marshmallow

Suppose i have 2 tables , Table 1 consist of users info and Table 2 consist of Branch info.
table 1 and table 2 is related to each other by many to many relationship.
e.g 1 user can work in multiple branches and 1 branch can have multiple users.
so here there's no parent child concept. i was wondering if i have to create another table with schema and relate it to user and branch table using foreign key or shall i create an association table.
I have done this :
class UserBranchMap(db.Model):
id = db.Column(db.Integer, primary_key=True)
branch_id = db.Column(db.Integer,db.ForeignKey('branch.id'))
branch = db.relationship("Branch", backref=db.backref("UBMbranch", lazy="dynamic"))
user_id = db.Column(db.Integer,db.ForeignKey('user.id'))
user = db.relationship("User", backref=db.backref("UBMuser", lazy="dynamic"))
created_at = db.Column(db.VARCHAR(20), nullable = False)
created_by = db.Column(db.VARCHAR(20), nullable = False)
class UserBranchMapSchema(ma.Schema):
branch = fields.Nested(branch_schema)
user = fields.Nested(user_schema)
class Meta:
fields = ('branch_id','user_id','created_at','created_by')
userbranchmap_schema = UserBranchMapSchema()
userbranchmaps_schema = UserBranchMapSchema(many = True)
what's the difference between association table and this mapping table ?
If I understand you correctly, you're asking about the difference between an association table
UserBranches = db.Table(
'user_branches',
db.Column('user_id', db.Integer, db.ForeignKey('user.id'), primary_key=True),
db.Column('branch_id', db.Integer, db.ForeignKey('branch.id'), primary_key=True)
)
and a mapping table
class UserBranch(db.Model):
id = db.Column(db.Integer, primary_key=True)
user = db.relationship("User", backref=db.backref("UBMuser", lazy="dynamic"))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
branch = db.relationship("Branch", backref=db.backref("UBMbranch", lazy="dynamic"))
branch_id = db.Column(db.Integer, db.ForeignKey('branch.id'))
On a database level, there is no real difference between them. If you only want to have a strict Many To Many relationship between two tables, just use an association table. Because of SQLAlchemy, you never have to do anything with it, because joining through this table happens automatically, as soon as you join Users to Branches or the reverse.
If, however, you want to do more, like have it actually denote a relationship, then the mapping table like you wrote it the way to go, because it behaves exactly like a normal table. This means you can use it like UserBranch.created_at or even query it directly if you want.

SQLAlchemy ForeignKey with dynamic PostgreSQL Schema

How might one use SqlAlchemy dynamic schema translation with foreign key relationships using the declarative ORM? Below is an example, of what I am trying to do for a multi-tenant DB (each 'user' mapping to a schema). I am not sure how one goes about setting ForeignKey constraints in SQLAlchemy
Assuming the following table declarations:
class BaseMixin():
__table_args__ = {'schema':'dynamic'}
class Image(Base, BaseMixin):
__tablename__ == 'image'
id = Column(Integer, primary_key=True, autoincrement=True)
class Point(Base, BaseMixin):
__tablename__ == 'point'
fk = Column(Integer, ForeignKey("image.id", ondelete="CASCADE"))
The Point table has a FK relationship to the Image table. I am getting the following error when I attempt to query these tables:
NoReferencedTableError: Foreign key associated with column 'point.fk' could not find table 'image' with which to generate a foreign key to target column 'id'
The session is parametrized with the following:
session.connection(execution_options={
"schema_translate_map": {"dynamic": "my_custom_schema"}})
From the docs, it looks like the schema_translate_map update on the session should be replacing dynamic with the user's schema. Oddly, the table instantiation is happening as expected. The error is popping up when attempting to query.
You may need to specify the schema in the ForeignKey i.e. update this:
fk = Column(Integer, ForeignKey("image.id", ondelete="CASCADE"))
to:
fk = Column(Integer, ForeignKey("dynamic.image.id", ondelete="CASCADE"))

Resources