10. ORM对关联表的操作:正向、反向访问

操作系统: windows
IDE: Pycharm

前面数据库的一对多,一对一,多对多,都是通过外键来实现。
接下来,通过一个实例演示,Django ORM如何操作外键关联关系,来实现根据条件进行查询。
ORM其实就是一种面向对象的思路。

首先在 models.py 中定义这样的两个Model,对应两张表:

# 国家表
class Country(models.Model):
    name = models.CharField(max_length=100)

# 学生表, country 字段是国家表的外键,形成一对多的关系
class Student(models.Model):
    name    = models.CharField(max_length=100)
    grade   = models.PositiveSmallIntegerField()
    country = models.ForeignKey(Country, on_delete=models.PROTECT)

还是老套路,在根目录cmd中:

  1. python manage.py makemigrations common
  2. python manage.py migrate

在数据库中生成两张表。
前面往数据库中填入消息在前端实现,其实也可以通过shell来实现:
在命令行执行:python manage.py shell,直接启动Django命令行,然后往里面输入以下数据:

from common.models import *
c1 = Country.objects.create(name='中国')
c2 = Country.objects.create(name='美国')
c3 = Country.objects.create(name='法国')
Student.objects.create(name='小明', grade=1, country=c1)
Student.objects.create(name='小华', grade=2, country=c1)
Student.objects.create(name='小陈', grade=1, country=c1)
Student.objects.create(name='小张', grade=3, country=c1)
Student.objects.create(name='Sheldon', grade=1, country=c2)
Student.objects.create(name='Penny',  grade=1, country=c2)
Student.objects.create(name='Leonard', grade=3, country=c2)
Student.objects.create(name='Howard', grade=2, country=c2)
Student.objects.create(name='Raj', grade=2, country=c3)

在这里插入图片描述
通过对象访问外键表:
如果你已经获取了一个student对象,要得到他的国家名称只需这样:

s1 = Student.objects.get(name='小明')
s1.country.name

.object.get()获得了一个实例,s1就是一个Student类的对象,s1.country就是外键所对应的对象,s1.country.name就获得了小明对应的国家的名称:
在这里插入图片描述


根据外键表数据过滤
如果需要查找Student表中所有一年级学生:

Student.objects.filter(grade=1).values()

现在如果需要查找Student表中一年级中国学生,该怎么做呢?

Student.objects.filter(grade=1,country='中国')

然而这样做并不可行。因为Student表中没有名为country的字段,只有一个名为country_id的外键。

正确的做法:可以先获取中国的国家id,然后再通过id去找,像这样:

cn = Country.objects.get(name='中国')
Student.objects.filter(grade=1,country_id=cn.id).values()
# 上面的cn.id直接用cn也是可以的

注意外键字段的id是通过后缀_id获取的。
上面的写法,有两步操作,会比较麻烦,事实上在Django ORM中,对外键关联,有更方便的语法:

Student.objects.filter(grade=1,country__name='中国').values()

注意:是两个下划线__,ORM会自动找country外键对应的表里面name='中国'的记录。一步到位,并且只需要发送一个数据库请求。

如果返回结果中只需要学生姓名国家名两个字段,可以这样指定values()内容:

Student.objects.filter(grade=1,country__name='中国').values('name','country__name')

在某些情况下,接口里规定的可能就是countryname,而不是country__name,可以使用 annotate 方法将获取的字段值进行重命名:
(annotate写在filter之前,annotate(新名字=F(‘老名字’)))

from django.db.models import F

# annotate 可以将表字段进行别名处理
Student.objects.annotate(
    countryname=F('country__name'),studentname=F('name')
    ).filter(grade=1,countryname='中国').values('studentname','countryname')

当接口要求和当前命名不一样的时候就可以用annotate来进行转换


反向访问:

如果已经获取了一个Country对象,如何访问到所有属于这个国家的学生呢?

cn = Country.objects.get(name='中国')
cn.student_set.all()

将Student转化为小写,然后加上_set来获取所有的反向外键关联对象。
在这里插入图片描述
然后可以用cn.student_set.all()[0].name这样来获取第一条对象的name的值,像对对象的操作一样。


Django还有一个方法。可以更直观的反映关联关系:
在定义Model的时候,在外键字段多定义一个 related_name 参数,像这样:

# 国家表
class Country(models.Model):
    name = models.CharField(max_length=100)

# country 字段是国家表的外键,形成一对多的关系
class Student(models.Model):
    name    = models.CharField(max_length=100)
    grade   = models.PositiveSmallIntegerField()
    country = models.ForeignKey(Country,
                on_delete = models.PROTECT,
                # 指定反向访问的名字
                related_name='students')

然后就可以使用更加直接的属性名:

cn = Country.objects.get(name='中国')
cn.students.all()

反向过滤:
如果需要获取所有一年级学生的国家名可以这样:

# 先获取所有一年级学生的id列表
country_ids=Student.objects.filter(grade=1).values_list('country',flat=True)

# 再通过id列表使用id__in 过滤
Country.objects.filter(id__in=country_ids).values()

注:

  1. flat=True就是直接把值拿出来组成QuerySet,而不是'键':'值'这样的形式。
  2. id__in,两个下划线,过滤用

这个写法需要访问数据库两次,性能也不佳。
在这里插入图片描述
反向访问也有Django ORM支持的更方便的写法:
没有定义反向related_name,就将类名Student改为小写,然后加两个下划线:

Country.objects.filter(students__grade=1).values()

注意: 这里由于在Models定义表的时候,用related_name='students'指定了反向关联名称students,所以这里是students__grade(两个下划线)。

但是会有重复字段,可以用.distinct()去重:
在这里插入图片描述
注意: 据说.distinct()对MySql数据库无效,实测对SQLite有效


应用到项目中:

首先先浏览订单相关的api文档:

列出所有订单
请求消息

GET  /api/mgr/orders  HTTP/1.1

请求参数
http 请求消息 url 中 需要携带如下参数:

  • action 填写值为 list_order

响应消息

HTTP/1.1 200 OK
Content-Type: application/json

响应内容
http 响应消息 body 中, 数据以json格式存储,如果获取信息成功,返回如下:

{
    "ret": 0,
    "retlist": [
        {id: 1, name: "华山医院订单001", create_date: "2018-12-26T14:10:15.419Z", customer_name: "华山医院",medicines_name: "青霉素"},
        {id: 2, name: "华山医院订单002", create_date: "2018-12-27T14:10:37.208Z", customer_name: "华山医院",medicines_name: "青霉素 | 红霉素 "}
    ]              
}
  • ret 为 0 表示登录成功
  • retlist 里面包含了所有的订单信息列表。

每个订单信息以如下格式存储:

{
    id: 2, 
    name: "华山医院订单002", 
    create_date: "2018-12-27T14:10:37.208Z", 
    customer_name: "华山医院",
    medicines_name: "青霉素 | 红霉素 "
}

其中 medicines_name 表示对应的药品,如果该订单有多个药品, 中间用 竖线隔开


添加一个订单
请求消息

POST  /api/mgr/orders  HTTP/1.1
Content-Type:   application/json

请求参数
http 请求消息 body 携带添加订单的信息

消息体的格式是json,如下示例:

{
    "action":"add_order",
    "data":{
        "name":"华山医院订单002",
        "customerid":3,
        "medicineids":[1,2]
    }
}

其中

  • action 字段固定填写 add_order 表示添加一个订单
  • data 字段中存储了要添加的订单的信息
  • medicineids 是 该订单中药品的id 列表

服务端接受到该请求后,应该在系统中增加这样的订单。

响应消息

HTTP/1.1 200 OK
Content-Type: application/json

响应内容
http 响应消息 body 中, 数据以json格式存储,

如果添加成功,返回如下

{
    "ret": 0,
    "id" : 677
}
  • ret 为 0 表示成功。
  • id 为 添加订单的id号。

如果添加失败,返回失败的原因,示例如下

{
    "ret": 1,    
    "msg": "订单名已经存在"
}
  • ret 不为 0 表示失败, msg字段描述添加失败的原因

修改订单
暂不支持

删除订单
暂不支持


在 mgr 目录下面新建 order.py 处理客户端发过来的列出订单、添加订单 的请求。同样,先写dispatcher函数,代码如下:

from django.http import JsonResponse
from django.db.models import F
from django.db import IntegrityError, transaction

# 导入 Order 对象定义
from  common.models import  Order,OrderMedicine

import json

def dispatcher(request):
    # 根据session判断用户是否是登录的管理员用户
    if 'usertype' not in request.session:
        return JsonResponse({
            'ret': 302,
            'msg': '未登录',
            'redirect': '/mgr/sign.html'},
            status=302)

    if request.session['usertype'] != 'mgr':
        return JsonResponse({
            'ret': 302,
            'msg': '用户非mgr类型',
            'redirect': '/mgr/sign.html'},
            status=302)


    # 将请求参数统一放入request 的 params 属性中,方便后续处理

    # GET请求 参数 在 request 对象的 GET属性中
    if request.method == 'GET':
        request.params = request.GET

    # POST/PUT/DELETE 请求 参数 从 request 对象的 body 属性中获取
    elif request.method in ['POST','PUT','DELETE']:
        # 根据接口,POST/PUT/DELETE 请求的消息体都是 json格式
        request.params = json.loads(request.body)

    # 根据不同的action分派给不同的函数进行处理
    action = request.params['action']
    if action == 'list_order':
        return listorder(request)
    elif action == 'add_order':
        return addorder(request)

    # 订单暂不支持修改和删除

    else:
        return JsonResponse({'ret': 1, 'msg': '不支持该类型http请求'})

记得添加路由!
接下来要具体的写listorderaddorder函数


添加订单addorder:

现在需要函数 addorder,来处理添加订单请求。
根据api文档,添加一条订单记录,需要在2张表(OrderOrderMedicine )中添加记录。

特别注意: 两张表的插入,意味着要有两次数据库操作。
如果第一次插入成功, 而第二次插入失败, 就会出现 Order表中把订单信息写了一部分,而OrderMedicine表中该订单的信息却没有写成功。

出大问题: 就会造成这个处理做了一半,那么数据库中就会出现数据的不一致。术语叫脏数据,数据库的事务机制来解决这个问题。

把一批数据库操作放在事务中, 该事务中的任何一次数据库操作失败了, 数据库系统就会让整个事务就会发生回滚,撤销前面的操作, 数据库回滚到这事务操作之前的状态,事务具有原子性,一个事务不可分割。

使用Django的 with transaction.atomic()来进行事务操作:

def addorder(request):
    info = request.params['data']

    # 从请求消息中获取要添加订单的信息 并且插入到数据库中
    # atomic原子性的,不可分割。 with transaction.atomic()下的都视为一次事务操作

    with transaction.atomic():
        new_order = Order.objects.create(name=info['name'],
                                         customer_id=info['customerid'])
        
        # batch列表用于存放药品记录,medicineids里有多少药品id就插入多少条记录
        batch = [OrderMedicine(order_id=new_order.id, medicine_id=mid, amount=1)
                 for mid in info['medicineids']]
        
        # bulk_create比用for循环来实现create性能更好,bulk_create参数是一个包含所有该表的 Model 对象的列表
        OrderMedicine.objects.bulk_create(batch)

    return JsonResponse({'ret': 0, 'id': new_order.id})

列出订单listorder:

listorder函数用来处理列出订单请求。

根据接口文档,我们应该返回订单记录格式,如下:

[
    {
        id: 1, 
        name: "华山医院订单001", 
        create_date: "2018-12-26T14:10:15.419Z",
        customer_name: "华山医院",
        medicines_name: "青霉素"
    },
    {
        id: 2, 
        name: "华山医院订单002", 
        create_date: "2018-12-27T14:10:37.208Z",
        customer_name: "华山医院",
        medicines_name: "青霉素 | 红霉素 "
    }
] 

其中 ‘id’,‘name’,‘create_date’ 这些字段的内容获取很简单,order表中就有这些字段,而:‘customer_name’ 和 ‘medicines_name’ 这两个字段都不在 Order表中,怎么获取?

Order 这个Model 中 有 ‘customer’ 字段 , 它外键关联了 Customer 表中的一个记录,这个记录里面 的 name字段 就是我们要取的字段。

获取外键关联的表记录的字段值,在Django中很简单,可以直接通过外键字段后面加两个下划线+关联字段名的方式来获取

def listorder(request):
    qs = Order.objects.values(
        'id', 'name', 'create_date',
        # 两个下划线,表示取customer外键关联的表中的name字段的值
        'customer__name'
    )

    # 将 QuerySet 对象转化为list类型
    retlist = list(qs)
    return JsonResponse({'ret': 0, 'retlist': retlist})

可以看到他只显示了订单名称,通过F12可以看到确实是能获取到客户名称的,但为什么不能显示呢?

答:因为ORM获取到的客户名称是customer__name,接口定义的是customer_name,药品名称medicines_name也是同理。可以用annotate()方法将获取:

from django.db.models import F

def listorder(request):
    qs = Order.objects.annotate(
        customer_name=F('customer__name'),
        medicines_name=F('medicines__name')
        
    ).values(
        'id', 'name', 'create_date',
        # 现在就可以用一个下划线的这个了
        'customer_name'
        'medicines_name'
    )

    # 将 QuerySet 对象转化为list类型
    retlist = list(qs)
    return JsonResponse({'ret': 0, 'retlist': retlist})

这是现在的成果:
在这里插入图片描述
然后需要修改一下数据的格式:

def listorder(request):
    # 返回一个 QuerySet 对象 ,包含所有的表记录
    qs = Order.objects\
            .annotate(
                customer_name=F('customer__name'),
                medicines_name=F('medicines__name')
            )\
            .values(
                'id','name','create_date','customer_name','medicines_name'
            )

    # 将 QuerySet 对象 转化为 list 类型
    retlist = list(qs)

    # 可能有 ID相同,药品不同的订单记录, 需要合并
    newlist = []
    id2order = {}
    for one in retlist:
        orderid = one['id']
        if orderid not in id2order:
            newlist.append(one)
            id2order[orderid] = one
        else:
            id2order[orderid]['medicines_name'] += ' | ' + one['medicines_name']

    return JsonResponse({'ret': 0, 'retlist': newlist})

在这里插入图片描述
就实现了有外键关联时候表的列出数据和添加数据的处理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据提供的错误信息,这是一个来自 elAdmin 项目的错误日志。错误信息显示了一个 `org.springframework.dao.DataIntegrityViolationException` 异常,并且还有一个 `org.hibernate.exception.DataException` 异常的嵌套异常。 根据错误信息,这个异常是由执行数据库语句时引发的。具体的 SQL 语句没有提供,因此无法确定具体的问题。但是,这个异常通常是由于数据完整性或类型不匹配导致的。 要解决这个问题,你可以考虑以下几种方法: 1. 检查数据完整性:检查你的代码中的数据操作(插入、更新或删除),确保数据的完整性约束得到满足。例如,检查是否有非空字段被设置为 null,或者外键关联是否存在。 2. 检查数据类型:检查数据操作中使用的数据类型是否与数据库定义的类型匹配。例如,确认字符串长度是否超过了数据库定义的长度限制,或者确认日期格式是否确。 3. 检查数据库定义:检查数据库定义,确保结构与你的代码操作一致。确认列的数据类型、长度、约束等是否确。 4. 检查数据库连接和配置:检查数据库连接和配置是否确。确保数据库服务常运行,并且连接参数、用户名和密码等配置确。 5. 检查数据库日志:如果可能的话,查看数据库的错误日志,以获取更详细的错误信息和上下文。 通过以上方法,你可以逐步排查并解决该错误。如果问题仍然存在,你可以提供更多相关的代码和错误信息,以便我能够更具体地帮助你解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值