sqlalchemy unique across multiple columns
Asked Answered
M

4

266

Let's say that I have a class that represents locations. Locations "belong" to customers. Locations are identified by a unicode 10 character code. The "location code" should be unique among the locations for a specific customer.

The two below fields in combination should be unique
customer_id = Column(Integer,ForeignKey('customers.customer_id')
location_code = Column(Unicode(10))

So if i have two customers, customer "123" and customer "456". They both can have a location called "main" but neither could have two locations called main.

I can handle this in the business logic but I want to make sure there is no way to easily add the requirement in sqlalchemy. The unique=True option seems to only work when applied to a specific field and it would cause the entire table to only have a unique code for all locations.

Mews answered 7/4, 2012 at 23:55 Comment(0)
A
453

Extract from the documentation of the Column:

unique – When True, indicates that this column contains a unique constraint, or if index is True as well, indicates that the Index should be created with the unique flag. To specify multiple columns in the constraint/index or to specify an explicit name, use the UniqueConstraint or Index constructs explicitly.

As these belong to a Table and not to a mapped Class, one declares those in the table definition, or if using declarative as in the __table_args__:

# version1: table definition
mytable = Table('mytable', meta,
    # ...
    Column('customer_id', Integer, ForeignKey('customers.customer_id')),
    Column('location_code', Unicode(10)),

    UniqueConstraint('customer_id', 'location_code', name='uix_1')
    )
# or the index, which will ensure uniqueness as well
Index('myindex', mytable.c.customer_id, mytable.c.location_code, unique=True)


# version2: declarative
class Location(Base):
    __tablename__ = 'locations'
    id = Column(Integer, primary_key = True)
    customer_id = Column(Integer, ForeignKey('customers.customer_id'), nullable=False)
    location_code = Column(Unicode(10), nullable=False)
    __table_args__ = (UniqueConstraint('customer_id', 'location_code', name='_customer_location_uc'),
                     )
Augustaaugustan answered 8/4, 2012 at 7:26 Comment(11)
I face the same problem also, but using UniqueConstraint didn't help me. After I try with Index('...') then I get a unique constraint. Is there any explanation with this behaviour?Malleolus
@swdev: which RDBMS do you use?Augustaaugustan
I am using PostgreSQL. Is there an issue with this?Malleolus
No, forget psql should work fine. Now: do you create a table using Location.create(engine) or meta.create_all()? Because declaring the UniqueConstraint like in the answer will only issue this part of the DDL. It will not on its own enforce the constraint.Augustaaugustan
I forgot to tell you that I am using Flask framework. So, it is part of model definition defined as a usual python class : class Branch(db.Models): ...' and UniqueConstraint` is located after and outsite the class indented block.Malleolus
Thanks, but my question was: did you use SA (and Flask) to create a DB schema, or did create it separately?Augustaaugustan
Why is the .c. used?Spanner
@Spanner .c. is a shortcut to .columns.Augustaaugustan
I'm trying this solution, but it doesn't seem to work for me. I'm not sure of the correct protocol in Stackoverflow to show you my code. If I open a new problem it will be probably be rejected as a duplicate. If I post it here, it appears like an answer. Any suggestions?Graph
...continuing from my previous comment. The answer doesn't seem to work for me. The only main difference between your solution and my code is that the foreign key for my table isn't in the UniqueConstraint. Does it have to be? The sqlalchemy docs seem to show UniqueConstraint as a positional parm, with ForeignKeyConstraint as the first parm. But your solution only shows UniqueConstraint in the tuple. Does I need to move the ForeignKeyConstrain from the normal definition into table_args? Maybe you someone could answer or revise to clarify these points?Graph
from sqlalchemy.schema import UniqueConstraintCinelli
C
47
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class Location(Base):
      __table_args__ = (
        # this can be db.PrimaryKeyConstraint if you want it to be a primary key
        db.UniqueConstraint('customer_id', 'location_code'),
      )
      customer_id = Column(Integer,ForeignKey('customers.customer_id')
      location_code = Column(Unicode(10))
Checkerwork answered 14/2, 2020 at 4:55 Comment(4)
Must be __table_args__ = (db.UniqueConstraint('customer_id', 'location_code'),), don't forget the comma at the end.Mariano
For anyone wondering why there’s that comma, (42) is the same as 42: parentheses have no effect around a single value. However, (42,) is shorthand for a tuple of a single element: (42,) == tuple([42]).Flagstone
This doesn't answer the question; the question doesn't mention Flask.Curative
To adapt, I'd need intimate knowledge of Flask. And if I knew how Flask did all that, I'd have no reason to visit this question in the first place. Therefore, this answer is bad. Mostly because it completely bypasses the parts of SQLAlchemy that the actual question is about.Curative
M
3

This Python3 answer using Flask and sqlalchemy is completely derivative, it just puts everything from above into a small self-contained working example for MySQL. I needed a uniqueness constraint on the table that implements a many-to-many relationship. Maybe you can run this to debug local environment problems, in my case they were purely between the keyboard and the chair :)

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship
import logging

logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://user:pass@localhost/mydb'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

user_role = db.Table(
    'user_role',
    Column('uid', String(6), ForeignKey('user.uid')),
    Column('role', String(32), ForeignKey('role.name')),
    UniqueConstraint('uid', 'role', name='idx_uid_role'))

class UserModel(db.Model):
    __tablename__ = 'user'
    uid = Column(String(6), primary_key=True)
    create_time = Column(Integer, nullable=False)
    login_time = Column(Integer, nullable=True)
    roles = relationship('RoleModel', secondary='user_role',
                         backref='user', lazy='joined')

class RoleModel(db.Model):
    __tablename__ = 'role'
    name = Column(String(32), primary_key=True)
    description = Column(String(256), nullable=False)

db.create_all()

After you run this, check the indexes defined for the table like this:

mysql> show index from user_role;

And you should see:

+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table     | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| user_role |          0 | idx_uid_role |            1 | uid         | A         |           0 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| user_role |          0 | idx_uid_role |            2 | role        | A         |           0 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| user_role |          1 | role         |            1 | role        | A         |           0 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
3 rows in set (0.00 sec)

Create test data:

mysql> insert into user values ('abc123', 1, 2);
mysql> insert into role values ('role1', 'Description');
mysql> insert into user_role (uid, role) values ('abc123', 'role1');

Finally, test the uniqueness constraint by running that last insert a second time, you should see:

mysql> insert into user_role (uid, role) values ('abc123', 'role1');
ERROR 1062 (23000): Duplicate entry 'abc123-role1' for key 'user_role.idx_uid_role'
Metrify answered 25/8, 2022 at 17:14 Comment(0)
C
2
__table_args__ = (
    Index("idx_room_user", room_id, uid, unique=True),
    UniqueConstraint(room_id, uid, name='u_room_user'),
)

It worked under sqlalchemy 2.0

Callow answered 16/2, 2023 at 15:16 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Oona

© 2022 - 2024 — McMap. All rights reserved.