python tutorial Mappage d'une classe sur plusieurs tables dans SQLAlchemy



sqlalchemy query (1)

En ce qui concerne KeyError : Les chaînes imprimées dans l'objet repr de l'objet __table__.columns NE SONT PAS les clés et, comme vous avez plusieurs colonnes id , un nom est en cours de traitement. Vous voudrez probablement faire "persons_id" plutôt que "persons.id" mais je vous conseille d'imprimer __table__.columns.keys() pour en être sûr.

En ce qui concerne AttributeError : SQLAlchemy mappe les noms de colonnes directement aux attributs par défaut, à moins que vous ne définissiez vous-même les mappages d'attributs que vous êtes. Le fait que vous définissiez l'attribut id tant que column_property sur column_property persons.c.id, users.c.id, user_groups.c.user_id signifie qu'aucune de ces colonnes n'est mappée directement sur un attribut de la classe ORM, mais ils seront toujours dans la collection de columns . Vous ne pouvez donc tout simplement pas utiliser les columns tant que liste de noms d'attributs.

Je n'ai pas reproduit l'intégralité de votre code / de vos données, mais j'ai mis en place un scénario de test plus simple avec 3 tables (incluant une relation m2m) pour vérifier ces éléments.

# ! /usr/bin/env python
# -*- coding: utf-8 -*-
# login_frontend.py

""" Python        2.7.3
    Cherrypy      3.2.2
    PostgreSQL    9.1
    psycopy2      2.4.5
    SQLAlchemy    0.7.10
"""

Je ne parviens pas à joindre quatre tables d'une classe Python / SQLAlchemy. J'essaie cela, donc je peux itérer l'instance de cette classe, au lieu du tuple nommé, que je tire de la jonction de tables avec l'ORM.

Pourquoi tout ça? Parce que j'ai déjà commencé comme ça et que je suis allé trop loin, pour le quitter. En outre, cela doit être possible, alors je veux savoir comment cela se passe.

Pour ce projet (cherrypy web-frontend), j'ai un module déjà terminé avec les classes de table. Je l'ai déplacé au bas de ce post, parce que peut-être que ce n'est même pas nécessaire pour vous.

Voici un exemple de tentative de tentative de classe de tables multiples jointe. J'ai choisi un cas simple avec plus que deux tables et une table de jonction. Ici, je n’écris pas dans ces tables jointes, mais c’est nécessaire ailleurs. C'est pourquoi les classes seraient une solution intéressante à ce problème.

Ma tentative de rejoindre la classe,

qui est une combinaison du module de classes de tables donné et des exemples de ces deux sites Web:

- Mappage d'une classe sur plusieurs tables
- SQLAlchemy: une classe - deux tables

class JoinUserGroupPerson (Base):

    persons = md.tables['persons']
    users = md.tables['users']
    user_groups = md.tables['user_groups']
    groups = md.tables['groups']

    user_group_person =(
        join(persons, users, persons.c.id == users.c.id).
        join(user_groups, users.c.id == user_groups.c.user_id).
        join(groups, groups.c.id == user_groups.c.group_id))

    __table__ = user_group_person

    """ I expanded the redefinition of 'id' to three tables,
        and removed this following one, since it made no difference:
        users_id = column_property(users.c.id, user_groups.c.user_id)
    """

    id = column_property(persons.c.id, users.c.id, user_groups.c.user_id)
    groups_id = column_property(groups.c.id, user_groups.c.group_id)
    groups_name = groups.c.name

    def __init__(self, group_name, login, name, email=None, phone=None):
        self.groups_name = group_name
        self.login = login
        self.name = name
        self.email = email
        self.phone = phone

    def __repr__(self):
        return(
            "<JoinUserGroupPerson('%s', '%s', '%s', '%s', '%s')>" %(
            self.groups_name, self.login, self.name, self.email, self.phone))

Différents accès à la table avec cette classe de jointure

  • Voici comment j'ai essayé d'interroger cette classe dans un autre module:

    pg = sqlalchemy.create_engine(
        'postgresql://{}:{}@{}:{}/{}'.
        format(user, password, server, port, data))
    Session = sessionmaker(bind=pg)
    s1 = Session()
    
    query = (s1.query(JoinUserGroupPerson).
        filter(JoinUserGroupPerson.login==user).
        order_by(JoinUserGroupPerson.id))
    
        record = {}
        for rowX in query:
            for colX in rowX.__table__.columns:
                record[column.name] = getattr(rowX,colX.name)
    
    
    """ RESULT:
    """
    
    
    Traceback (most recent call last):
      File "/usr/local/lib/python2.7/dist-packages/cherrypy/_cprequest.py", line 656, in respond
        response.body = self.handler()
      File "/usr/local/lib/python2.7/dist-packages/cherrypy/lib/encoding.py", line 228, in __call__
        ct.params['charset'] = self.find_acceptable_charset()
      File "/usr/local/lib/python2.7/dist-packages/cherrypy/lib/encoding.py", line 134, in find_acceptable_charset
        if encoder(encoding):
      File "/usr/local/lib/python2.7/dist-packages/cherrypy/lib/encoding.py", line 86, in encode_string
        for chunk in self.body:
      File "XXX.py", line YYY, in ZZZ
        record[colX.name] = getattr(rowX,colX.name)
    AttributeError: 'JoinUserGroupPerson' object has no attribute 'user_id'
  • Puis j'ai vérifié les attributs de la table:

    for rowX in query:
        return (u'{}'.format(rowX.__table__.columns))
    
    
    """ RESULT:
    """
    
    
    ['persons.id',
     'persons.name',
     'persons.email',
     'persons.phone',
     'users.id',
     'users.login',
     'user_groups.user_id',
     'user_groups.group_id',
     'groups.id',
     'groups.name']
  • Ensuite, j'ai vérifié, si la requête ou ma classe ne fonctionnait pas du tout, en utilisant un compteur. Je me suis levé à (compte == 5), donc les deux premières tables jointes. Mais lorsque j'ai défini la condition sur (nombre == 6), j'ai reçu à nouveau le premier message d'erreur. AttributeError: l'objet 'JoinUserGroupPerson' n'a pas d'attribut 'id_utilisateur':

    list = []
    for rowX in query:
        for count, colX in enumerate(rowX.__table__.columns):
            list.append(getattr(rowX,colX.name))
            if count == 5:
                break
    return (u'{}'.format(list))
    
    
    """ RESULT:
    """
    
    
    [4, u'user real name', None, None, 4, u'user']
    
    
    """ which are these following six columns:
        persons[id, name, email, phone], users[id, login]
    """
  • Puis j'ai vérifié chaque colonne:

    list = []
    for rowX in query:
        for colX in rowX.__table__.columns:
            list.append(colX)
    return (u'{}'.format(list))
    
    
    """ RESULT:
    """
    
    
    [Column(u'id', INTEGER(), table=, primary_key=True, nullable=False, server_default=DefaultClause(, for_update=False)),
     Column(u'name', VARCHAR(length=252), table=, nullable=False),
     Column(u'email', VARCHAR(), table=),
     Column(u'phone', VARCHAR(), table=),
     Column(u'id', INTEGER(), ForeignKey(u'persons.id'), table=, primary_key=True, nullable=False),
     Column(u'login', VARCHAR(length=60), table=, nullable=False),
     Column(u'user_id', INTEGER(), ForeignKey(u'users.id'), table=, primary_key=True, nullable=False),
     Column(u'group_id', INTEGER(), ForeignKey(u'groups.id'), table=, primary_key=True, nullable=False),
     Column(u'id', INTEGER(), table=, primary_key=True, nullable=False),
     Column(u'name', VARCHAR(length=60), table=, nullable=False)]
  • Ensuite, j'ai essayé deux autres accès directs, ce qui m'a valu les deux KeyErrors pour 'id' et 'personnes.id':

    for rowX in query:
        return (u'{}'.format(rowX.__table__.columns['id'].name))
    
    for rowX in query:
        return (u'{}'.format(rowX.__table__.columns['persons.id'].name))

Conclusion

J'ai essayé quelques autres choses, qui étaient encore plus déroutantes. Comme ils ne révélaient plus d'informations, je ne les ai pas ajoutés. Je ne vois pas où ma classe a tort.

J'imagine que, d'une manière ou d'une autre, j'ai dû définir la classe de manière à ne joindre correctement les deux premières tables. Mais la jointure fonctionne au moins partiellement, car lorsque la table 'user_groups' était vide, j'ai également une requête vide.

Ou peut-être que j'ai fait quelque chose de mal avec le mappage de cette table 'user_groups'. Comme avec la jointure, certaines colonnes sont doubles, elles nécessitent une définition supplémentaire. Et le 'user_id' fait déjà partie de la table des personnes et des utilisateurs, j'ai donc dû le mapper deux fois.

J'ai même essayé de supprimer la table 'user_groups' de la jointure, car elle se trouve dans les relations (avec les secondaires). Cela m'a donné un message d'erreur de clé étrangère. Mais peut-être que je me suis trompé.

Certes, je ne sais même pas pourquoi ...

rowX.__table__.columns                  # column names as table name suffix 

... a des noms d'attribut différents de ...

colX in rowX.__table__.columns        # column names without table names

Extra Edits

  • Une autre pensée! Tout cela serait-il possible avec l'héritage? Chaque classe a son propre mappage, mais la classe user_groups peut alors être nécessaire. Les jointures devaient être entre les classes simples à la place. Les init () et repr () devaient encore être redéfinis.

  • Cela a probablement quelque chose à voir avec la table 'user_groups', car je ne pouvais même pas la joindre à la table 'groups' ou 'users'. Et il est toujours dit que l'objet de classe n'a pas d'attribut 'user_id'. Peut-être que c'est quelque chose à propos de la relation plusieurs-à-plusieurs.

Attachement

Voici le module SQLAlchemy déjà donné, avec en-tête, sans informations spécifiques sur la base de données et les classes des tables jointes:

#!/usr/bin/python
# vim: set fileencoding=utf-8 :

import sqlalchemy
from sqlalchemy import join
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref, column_property

pg = sqlalchemy.create_engine(
    'postgresql://{}@{}:{}/{}'.format(user, host, port, data))

md = sqlalchemy.MetaData(pg, True)
Base = declarative_base()



""" ... following, three of the four joined tables.
    UserGroups isn't necessary, so it wasn't part of the module.
    And the other six classes shouldn't be important for this ...
"""


class Person(Base):
    __table__ = md.tables['persons']

    def __init__(self, name, email=None, phone=None):
        self.name = name
        self.email = email
        self.phone = phone

    def __repr__(self):
        return(
            "<Person(%s, '%s', '%s', '%s')>" %(
            self.id, self.name, self.email, self.phone))

class Group(Base):
    __table__ = md.tables['groups']

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return("<Group(%s, '%s')>" %(self.id, self.name))

class User(Base):
    __table__ = md.tables['users']

    person = relationship('Person')
    groups = relationship(
        'Group', secondary=md.tables['user_groups'], order_by='Group.id',
        backref=backref('users', order_by='User.login'))

    def __init__(self, person, login):
        if isinstance(person, Person):
            self.person = person
        else:
            self.id = person
        self.login = login

    def __repr__(self):
        return("<User(%s, '%s')>" %(self.id, self.login))

Peut-être que le script suivant, qui a créé la base de données et qui a déjà été donné, sera utile ici. La dernière partie contient des données de test, mais entre les colonnes sont supposés être des tabulations, pas d'espaces. A cause de cela, ce script peut également être trouvé comme élément essentiel sur github :

-- file create_str.sql
-- database creation script
-- central script for creating all database objects

-- set the database name
\set strdbname logincore

\c admin

BEGIN;
\i str_roles.sql
COMMIT;

DROP DATABASE IF EXISTS :strdbname;
CREATE DATABASE :strdbname TEMPLATE template1 OWNER str_db_owner
    ENCODING 'UTF8';
\c :strdbname

SET ROLE str_db_owner;

BEGIN;
\i str.sql
COMMIT;
RESET ROLE;





-- file str_roles.sql
-- create roles for the database

-- owner of the database objects
SELECT create_role('str_db_owner', 'NOINHERIT');

-- role for using
SELECT create_role('str_user');

-- make str_db_owner member in all relevant roles
GRANT str_user TO str_db_owner WITH ADMIN OPTION;





-- file str.sql
-- creation of database

-- prototypes
\i str_prototypes.sql

-- domain for non empty text
CREATE DOMAIN ntext AS text CHECK (VALUE<>'');

-- domain for email addresses
CREATE DOMAIN email AS varchar(252) CHECK (is_email_address(VALUE));

-- domain for phone numbers
CREATE DOMAIN phone AS varchar(60) CHECK (is_phone_number(VALUE));

-- persons
CREATE TABLE persons (
    id    serial       PRIMARY KEY,
    name  varchar(252) NOT NULL,
    email email,
    phone phone
);

GRANT SELECT, INSERT, UPDATE, DELETE ON persons TO str_user;
GRANT USAGE ON SEQUENCE persons_id_seq TO str_user;

CREATE TABLE groups (
    id   integer     PRIMARY KEY,
    name varchar(60) UNIQUE NOT NULL
);

GRANT SELECT ON groups TO str_user;

-- database users
CREATE TABLE users (
    id    integer     PRIMARY KEY REFERENCES persons(id) ON UPDATE CASCADE,
    login varchar(60) UNIQUE NOT NULL
);

GRANT SELECT ON users TO str_user;

-- user <-> groups
CREATE TABLE user_groups (
    user_id  integer NOT NULL REFERENCES users(id)
                              ON UPDATE CASCADE ON DELETE CASCADE,
    group_id integer NOT NULL REFERENCES groups(id)
                              ON UPDATE CASCADE ON DELETE CASCADE,
    PRIMARY KEY (user_id, group_id)
);

-- functions
\i str_functions.sql





-- file str_prototypes.sql
-- prototypes for database

-- simple check for correct email address
CREATE FUNCTION is_email_address(email varchar) RETURNS boolean
    AS $CODE$
    SELECT FALSE
    $CODE$ LANGUAGE sql IMMUTABLE STRICT;

-- simple check for correct phone number
CREATE FUNCTION is_phone_number(nr varchar) RETURNS boolean
    AS $CODE$
    SELECT FALSE
    $CODE$ LANGUAGE sql IMMUTABLE STRICT;





-- file str_functions.sql
-- functions for database

-- simple check for correct email address
CREATE OR REPLACE FUNCTION is_email_address(email varchar) RETURNS boolean
    AS $CODE$
    SELECT $1 ~ E'^[A-Za-z0-9.!#$%&\'\*\+\-/=\?\^_\`{\|}\~\.][email protected][-a-z0-9\.]+$'
    $CODE$ LANGUAGE sql IMMUTABLE STRICT;

-- simple check for correct phone number
CREATE OR REPLACE FUNCTION is_phone_number(nr varchar) RETURNS boolean
    AS $CODE$
    SELECT $1 ~ E'^[-+0-9\(\)/ ]+$'
    $CODE$ LANGUAGE sql IMMUTABLE STRICT;





-- file fill_str_test.sql
-- test data for database
-- between the columns are supposed to be tabs, no spaces !!!

BEGIN;

COPY persons (id, name, email) FROM STDIN;
1   Joseph Schneider    [email protected].uni.de
2   Test User   [email protected].uni.de
3   Hans Dampf  \N
\.
SELECT setval('persons_id_seq', (SELECT max(id) FROM persons));

COPY groups (id, name) FROM STDIN;
1   IT
2   SSG
\.

COPY users (id, login) FROM STDIN;
1   jschneid
2   tuser
3   dummy
\.

COPY user_groups (user_id, group_id) FROM STDIN;
1   1
2   1
3   2
\.

COMMIT;




cherrypy