“安居客“住房系统-基于Python-Django前后端分离开发(二)——基于RESTful架构的数据接口配置以及Redis高速缓存

"安居客"住房系统-基于Python-Django前后端分离开发(二)

基于Django-Rest-Framework创建接口数据(二)


通过ORM框架,直接通过面向对象的方式对模型进行查询操作。由于模型都是objects的属性,它是模型对象的管理工具,提供了CRUD的方法。

省市区三级联动数据接口

创建省级行政区的数据接口

api文件中为其创建一个简单的序列化器DistrictSimpleSerializers,只需要序列省级行政区的地区编号和名称。

api文件中新建serializers文件,用来书写FBV的序列化器

class DistrictSimpleSerializers(serializers.ModelSerializer):

    class Meta:
        model = District
        fields = ('distid', 'name')

commonview.py中编写查询省级行政区的视图函数get_provinces

@api_view(('GET', ))
def get_provinces(request: HttpRequest) -> HttpResponse:
    queryset = District.objects.filter(parent__isnull=True)
    serializer = DistrictSimpleSerializers(queryset, many=True)
    return Response({
        'code': 10000,
        'message': '获取省级行政区成功',
        'result': serializer.data
    })

【说明】

  1. 判断父级行政区为空,需要用isnull属性而不能用parent=NULL
  2. 拿到queryset查询集,需要对其进行序列化操作。所谓序列化,就是把一个对象变成字符串或是字节串;反序列化,激素hi将一个字符串或是字节串还原成对象的过程。
  3. 此处由于返回Response,需要加上装饰器才能执行。

将Django数据迁移到数据库中,避免与浏览器缓存数据发生冲突,导致无法查询到数据接口。

# 在终端中执行命令
python manage.py migrate sessions

api/urls.py文件添加视图函数路由

urlpatterns = [
    path('districts/', get_provinces)
]

如果希望请求途中包含api,还需要配置全局url,在Django项目的urls.py文件中

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls'))
]

完成好以上操作,运行项目,在浏览器中请求api/districts数据接口,就可以查询到行政区的JSON数据了

在这里插入图片描述

在页面左侧调试栏中的SQL语句中发现,除了查询distidname,其他的字段也都被查询出来了,这将影响SQL语句的查询效率,这时需要在查询集添加only()属性指定需要查询的字段,也可以使用defer()指定不需要查询的字段,下文不再赘述。

queryset = District.objects.filter(parent__isnull=True).only('distid', 'name')

在这里插入图片描述

查询每个省的城市/区/县的信息

每个地区都有自己对应的编号,例如四川的编号为510000,成都的编号为510100,若要查询对应市/区的信息。首先配置api/urls.py文件

path('districts/<int:distid>', get_cities)

当我们在浏览器中请求api/districts/510000就可以拿到四川省各个城市的信息,如果请求510100就能够拿到成都市各个区县的信息,所以接下来的视图函数需要返回各个城市的地区编号,将函数命名为get_cities。由于需要根据传入的实参查询到地区编号,再根据地区编号查询到对应的地区,这里先来编写查询城市信息的序列化器。

class DistrictDetailSerializer(serializers.ModelSerializer):
    cities = serializers.SerializerMethodField()

    @staticmethod
    def get_cities(district):
        queryset = District.objects.filter(parent__distid=district.distid)
        return DistrictSimpleSerializers(queryset, many=True).data

    class Meta:
        model = District
        exclude = ('parent', 'ishot')

【说明】

parent__distid=district.distid:找到父级行政区的distid和传入的参数distid相同的字段,返回父级行政区为parent__distid的对象,如果参数为510100,则返回父级行政区为510100的城市,也就是返回510100对应的区县。

查询城市数据接口的视图函数如下:

@api_view(('GET', ))
@cache_page(timeout=600, cache='default')
def get_cities(request, distid):
    district = District.objects.get(distid=distid)  # 根据主键查询编号,通过编号拿到地区
    serializer = DistrictDetailSerializer(district)
    return Response(serializer.data)

我们同样将城市接入Redis数据库缓存,再进入浏览器中查询api/districts/510000会看到如下信息。

在这里插入图片描述

查询api/districts/510100会看到如下信息。

在这里插入图片描述

查询热门城市信息

虽然FBV比较灵活,但是FBV的api数据接口的代码量比较多,比较冗杂,接下来查询热门城市信息,我将会用CBVj进行书写。

class HotCitiesView(ListAPIView):
    queryset = District.objects.filter(ishot=True)
    serializer_class = DistrictSimpleSerializers

因为我们需要的只是查询到热门城市的信息,而不是对其进行增删改操作,所以此时只需要传给类只做查询功能的ListAPIView,此处只能返回一个列表。此处依然通过DistrictSimpleSerializers就可以对查询到的热门城市进行序列化。由于HotCitiesView是一个类而不是函数,所以在添加HotCitiesViewurl映射时,会和函数稍有差别时。

path('hotcities/', HotCitiesView.as_view())

通过as_view()可以将类直接变成视图函数。

修改好了之后,我们就可以请求http://127.0.0.1:8000/api/hotcities/拿到热门城市的信息

在这里插入图片描述

但是此时返回的是一个列表而不是我们希望的字典,这时我们可以重写在ListAPIView类中的get方法,此方法返回的是一个响应对象,调用其父类的方法拿到返回值。在得到返回值之前,我们可以添加自己的业务逻辑。若要返回字典,可以自定义重新定制返回值。

 # HotCitiesView类中添加函数
 def get(self, request, *args, **kwargs):
        resp = super().get(request, *args, **kwargs)
        return Response({
            'code': 10000,
            'message': '获取热门城市数据成功',
            'result': resp.data
        })

(重点)设置经理人数据接口

此方法和返回热门城市的数据接口代码类似,详细代码间我的代码库 ,但是如果不想返回经理人,只想返回指定经理人的详细信息,这时可以在类中继承RetrieveAPIView,用过其中的self.retrieve获取单个对象。这时还需要调整urlpath('aagents/<int:pk>', AgentView.as_view()),资源标识符<int:pk>【说明:DRF框架默认参数pk】。

如继承RetrieveUpdateAPIView,则可以在数据接口的页面对经理人的详细信息做更新操作。

如果想要新增经理人信息,还可以进行多重继承ListCreateAPIView。由于想要新增经理人,此处发给服务器的是POST请求,此时还需要新增加一个url映射

path('angets/', AgentView.as_view()),

但是若进行多重继承,可能会出现MRO问题method resolution order。在继承的两个父类中,都会又get方法,这时在服务器请求api/agents/1想要拿到单个经理人的信息时,返回的数据依然是经理人列表。这时可以通过重写get方法来解决这一问题,为其加上分支结构来处理。条用DEBUG模式可以发现,在RetrieveUpdateAPIView的对象中会带有pk = {<实参>},所以这就是建立分支结构的依据:

def get(self, request, *args, **kwargs):
	cls = RetrieveUpdateAPIView if 'pk' in kwargs else ListCreateAPIView
	return cls.get(self, request, *args, **kwargs)

当我们这样修改了之后,尽管可以成功得分别请求两个数据接口的,但是新的问题又出现了。在新增经理人信息中,只有('agentid', 'name', 'tel', 'servstar'),因为序列化器中只有这四个字段。 而在数据库中要求realstar字段也不能为空,并且若要新增经理人,还需要为其添加其他的信息,在目前的经理人序列化器中是没有的。若此时提交POST请求,服务器就会报错。所以查看经理人的序列化器和新增经理的序列化器不能是同一个序列化器。此时新增一个添加经理人的序列化器。

class AgentCreateSerializer(serializers.ModelSerializer):
    """创建经理人"""
    class Meta:
        model = Agent
        exclude = ('estate', )

这时还要在经理人视图函数中做一个判断,若要查询经理人列表,用AgentSimpleSerializer,若要创建经理人或者查询经理人详细信息,用AgentCreateSerializer

    def get_serializer_class(self):
        return AgentCreateSerializer if self.request.method == 'POST' else AgentSimpleSerializer

若在查看经理人详情的时,想要看到经理人的所有信息,这时AgentSimpleSerializer就不再适用了,这时还需要调整代码,创建一个可以序列化经理人完整信息的序列化器,通过fields = "__all__"来实现。但是经理人所管理的楼盘,这样做了显示出来的只是所对应楼盘的编号,我们期望的是显示楼盘的具体名称。所以这时需要重新定义estates这个类的序列化方法。

class AgentDetailSerializer(serializers.ModelSerializer):
    """经理人详情"""
    estates = serializers.SerializerMethodField()

    @staticmethod
    def get_estates(agent):
        queryset = agent.estates.all()[:5]
        return EstateSimpleSerializer(queryset, many=True).data

    class Meta:
        model = Agent
        fields = "__all__"


class EstateSimpleSerializer(serializers.ModelSerializer):
    """楼盘简单信息"""
    class Meta:
        model = Estate
        fields = ('estateid', 'name')

并且修改经理人的视图函数:

class AgentView(RetrieveUpdateAPIView, ListCreateAPIView):
    """经理人视图"""

    def get_queryset(self):
        queryset = Agent.objects.all()
        if 'pk' not in self.kwargs:
            # 查询列表
            queryset = queryset.only('name', 'tel', 'servstar')
        else:
            queryset = queryset.prefetch_related(
                Prefetch('estates',
                         queryset=Estate.objects.all().only('name').order_by('-hot'))
            )
            if queryset.certificated == 1:
                queryset.certificated = '真的'
        return queryset

    def get_serializer_class(self):
        if self.request.method == 'POST':
            return AgentCreateSerializer
        else:
            return AgentDetailSerializer if 'pk' in self.kwargs else AgentSimpleSerializer

    def get(self, request, *args, **kwargs):
        cls = RetrieveUpdateAPIView if 'pk' in kwargs else ListCreateAPIView
        return cls.get(self, request, *args, **kwargs)
优化查询

在我们已经配置好的django-debug-toolar侧边连中会发现,SQL语句中会出现大量的重复查询,而且是没有用的查询语句,这就使得查询变得缓慢,一旦需要查询大量的数据,这将严重拖延查询时间,这时我们需要优化SQL查询。对于一对一或者多对一,可以通过select_related()关联对象,ORM框架就就可以生成内连接或者左外连接避免1+n查询问题。对于多对多关联,通过prefetch_related('estates')关联对象。

设置返回房屋信息的数据接口

配置房屋户型的数据接口(定制CRUD全套接口)

由于房屋的详细信息可以能时常发生变动,我们依然通过CBV创建一个类来返回房屋数据接口,并继承ModelViewSet可以进行房屋数据的增删改查操作

class HouseTypeViewSet(ModelViewSet):
    queryset = HouseType.objects.all()
    serializer_class = HouseTypeSerializer

但是通过ModelViewSet做的数据接口需要在api/urls.py注册路由

router = DefaultRouter()
router.register('housetypes', HouseTypeViewSet)
urlpatterns += router.urls

当我们请求api/housetype就可以返回户型的数据

在这里插入图片描述

并且在页面底部还可以进行增删改查操作

设置楼盘信息数据接口

在此处同经理人数据接口类似,同样分别可以查询楼盘列表和单个楼盘的详细信息。并用通过action发出的不同请求序列化对应的不同字段,拿到我们想要的数据。这里继承了ReadOnlyModelViewSet表示只读的一个接口。

视图函数如下:

class EstateViewSet(ReadOnlyModelViewSet):
    """楼盘视图"""
    queryset = Estate.objects.all()

    def get_queryset(self):
        if self.action == 'list':
            queryset = self.queryset.only("name")
        else:
            queryset = self.queryset.defer('district__parent', 'district__ishot',
                                           'district__intro').select_related('district')
        return queryset

    def get_serializer_class(self):
        return EstateDetailSerializer if self.action == 'retrieve' else EstateSimpleSerializer

楼盘序列化器如下:

class EstateSimpleSerializer(serializers.ModelSerializer):
    """楼盘简单序列化器"""

    class Meta:
        model = Estate
        fields = ('estateid', 'name')


class EstateDetailSerializer(serializers.ModelSerializer):
    """楼盘详情序列化器"""
    district = DistrictSimpleSerializers()

    class Meta:
        model = Estate
        fields = '__all__'

Django框架下原生SQL查询

通过django.db.connextions['default']拿到默认数据库的连接,返回连接代理对象,通过cursor()拿到游标,通过execute执行查询语句。在我的码云中有一个基于MySQL实现的python面向对象编程,可以在https://gitee.com/mysql_python中查看。

实现数据接口的分页功能

在DRF框架中,已经封装好了三种分页的类,在settings.py 文件中,在djangorestframework的配置代码这里

REST_FRAMEWORK = {
    # 配置默认页面大小
    'PAGE_SIZE': 5,
    # 配置默认的分页类
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
}

配置好就可以在接口文档中看到已经被分页了。但是framew自带的分页功能还存在一定的瑕疵,对于CBV的数据接口,就不会进行分页,只对FBV的数据接口才显示分页。对于一些数据较少的接口,像户型的数据接口,只有8个数据,就可以不需要进行分页的功能。对于不需要分页的数据接口,只需要在视图函数这里添加pagination_class = None就表示不需要分页器。

在浏览器的地址栏中,我们可以通过api/agents/?page=2来跳转到第二页,但是想要通过api/agents/?size=3来修改页面的大小则不会被执行。若希望能够执行我们期望的分页功能,这时可以自定义一个分页类,来继承PageNumberPagination这个父类。

api应用中新建一个helper.py文件来写帮助类。在PageNumberPagination这个类中会发现page_size_query_param = None,这就是我们需要进行修改的代码,当允许用户指定页面的大小是,我们还需要一个参数来限制最大的页面大小,在PageNumberPagination也能找到max_page_size = None

from rest_framework.pagination import PageNumberPagination


class CustomPagination(PageNumberPagination):
    page_size_query_param = 'size'
    max_page_size = 50

写好了自定义分页器,这时需要修改配置文件,让DEFAULT_PAGINATION_CLASS指定到我们自定义的分页类

REST_FRAMEWORK = {
    # 配置默认页面大小
    'PAGE_SIZE': 5,
    # 配置默认的分页类
    'DEFAULT_PAGINATION_CLASS': 'DEFAULT_PAGINATION_CLASS': 'api.helper.CustomPagination'
}

至此,分页功能就完成了。

但是这种分页的操作很容易暴露数据的规模,在Django中还有一种分页被称为"游标分页"。游标分页需要对数据进行排序后才能分页,这里也需要我们自定义一个游标分页类,并继承CustomPagination

class AgentCursorPagination(CustomPagination):
    page_size_query_param = 'size'
    max_page_size = 50
    ordering = '-agentid'

并在经理人视图函数添上这么一句话

pagination_class = AgentCursorPagination

就能够实现游标分页功能。

在这里插入图片描述

这时网站数据规模的相关信息已经被改成了一窜字母。

添加Redis高速缓存(空间换时间)

缓存是典型的时间换空间策略,池化作用(线程池、连接池等)也是典型的空间换时间的策略。缓存分为声明式缓存和编程式缓存。

声明式缓存

对于咱们国家,省级行政区域基本不会变动,所以像这样数据体量不大,又不会变动,又需要经常查询到的数据,应该将其保存到缓存中,不需要每次都查询。当我们把djangodebugtoolbar配置好后,可以在调试栏中发现,每次请求数据都需要重新查询,这样会增加数据库的压力。

这里我们为get_provinces视图函数加上一个声明式缓存@cache_page的装饰器。此处咱们默认使用Redis的0号服务器。

@cache_page(timeout=365 * 86400, cache='default')

将缓存存活的时间设置为一年,当我们再次请求接口,会发现在debug-toolbar的右侧工具调试栏中,SQL语句的查询时间已经变成了0.00ms。

为了减轻数据库的压力,我们依然要给HotCitiesView类添加一个装饰器提供Redis高速缓存。但是cache_page这个装饰器只能装饰函数而不能装饰类.

所以这里需要用@method_decorator(decorator=cache_page(timeout=86400, cache='default')),它能够将装饰函数的装饰器转换成装饰类的装饰器,由于ListAPIView是通过get方法查询到数据,所以还需要给这个装饰器添加一个属性name=get,此时这个装饰就就完善了。

@method_decorator(decorator=cache_page(timeout=86400, cache='default'), name='get')

当我们再次请求api/hotcities接口时,这时已经从Redis缓存中拿到数据而不是重新查询了。

对于ModelViewSet,增删改操作不需要假如缓存,缓存主要是要加在查询上, 不管是查单个韩式查询列表,主要是给listretrieve这两个方法加装饰器。若对查询列表的装饰器加缓存可以添加属性name='list',若对查单个信息加缓存可以添加属性name='retrieve'

@method_decorator(cache_page(timeout=86400, cache='default'), name='list')
@method_decorator(cache_page(timeout=86400, cache='default'), name='retrieve')

对于其他需要加缓存的类或者函数,我们都可以采用如上的方法加入缓存,下文不再赘述。

编程式缓存

但是对于声明式缓存,非常不灵活,而且装饰器的名字很长,这时我们也可以利用编程式缓存定制相对灵活的缓存方法。这里以get_cities视图函数为例进行讲解。

 district = caches['defualt'].get(f'district: {distid}')
    if district is None:
        district = District.objects.filter(distid=distid).defer('parent').first()
        caches['defualt'].set(f'district:{distid}', district)

这里先判断数据空中能不能拿到地区的缓存,如果有就走缓存那数据,如果没有就将数据进行缓存。但是Django封装的cache、caches只能通过set/get方法操作字符串数据类型。最直接的方法就是直接拿到redis连接,对redis进行操作,高度定制我们想要的缓存,代码如下:

@api_view(('GET', ))
def get_district(request: HttpRequest, distid) -> HttpResponse:
    """获取地区详情"""
    redis_cli = get_redis_connection()
    data = redis_cli.get(f'zufang:district:{distid}')
    if data:
        # 反序列化
        district = pickle.loads(data)
    else:
        district = District.objects.filter(distid=distid).defer('parent').first()
        data = pickle.dumps(district)
        # 序列化对象到redis缓存
        redis_cli.set(f'zufang:district:{distid}', data, timeout=900)
    serializer = DistrictDetailSerializer(district)
    return Response(serializer.data)

通过get_redis_connection()可以执行几乎所有的redis命令,但是这里需要手动序列化数据,可以利用pickle或者是json

接口限流

由于咱们的api接口时开放的,所以需要对接口做一个访问限速组织用户频繁的访问接口。在DRF框架中,已经存在限流的配置,这时我们只需要将配置添加上即可。但是若要限流,必须要配置缓存才行。我们可以将IP地址作为对应缓存的键,访问次数对应值,设置访问次数的限制后,随着访问次数的增加,值减少最后归零,达到组织访问的效果。

# # djangorestframework的配置
REST_FRAMEWORK = {
    
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '30/min',
        'user': '10000/day',
    }
}

这里分别对匿名用户和已有用户进行限流,匿名用户每分钟最多访问30次,已有用户每天可以访问1000次。若想要自定义限流类,我们可以继承UserRateThrottle,对其get_cache_key()函数进行重写。

但是想针对某一接口不限流或者需要特殊的限流策略,对于CBV视图函数,可以加上throttle_classes = (A, B, C)继承对应的自定义限流类;对于FBV视图函数,可以加上装饰器@throttle_classes(A, B, C)添加期望的限流方法。

高级接口数据筛选

指定条件对接口进行筛选,这里以经理人的视图函数为例进行讲解。我们很有可能指定经理人的服务星级或名字经行筛选。

需要找到需要筛选的api接口的视图函数,在中间添加get_queryset(self)的一个函数,self表示这个视图集对象,通过self.request.GET.get()获取到请求的参数,筛选条件一般跟在请求参数的后面,让其作为条件对queryset进行filter操作实现对数据的筛选。

agents的视图函数中,添加了筛选nameservstar的方法,这里就有了两个filter,这里的两个筛选条件时而且的关系,若想要做成Q对象,需要引用Q对象:Q & Q;Q | Q;-Q。所以若在浏览器中请求agents/?name='袁'&servstar=表示只返回名字包含“袁”的经理人,若请求api/agents/?name=袁&servstar=5则会返回姓名包含”袁“并且服务星级是五星级的经理人的信息。

name = self.request.GET.get('name')
        if name:
            # where name like '<name>'
            self.queryset = self.queryset.filter(name__startwith=name)
        servstar = self.request.GET.get('servstar')
        if servstar:
            # where servstar >= 参数传过来的星级
            # gte ---> great than or equal to
            self.queryset = self.queryset.filter(servstar__gte=servstar)

添加索引

在通过经理人筛选的时候,我们一般会查询其姓名而不会查询其编号,但是通过查询姓名会使得数据库得查询性能变得非常糟糕,这里需要为姓名添加索引。在数据库中,可以通过如下代码建立索引:

crete index idx_agnet_name on tb_agent(ename)

当执行select * from tb_agent where name='<name>'就不会进行全表查询,明显优化得查询性能。这种不是通过主键进行得索引查询称为非聚集查询。若名称很长,再为其添加索引会耗费空间资源,这时可以添加前缀索引:

drop index idx_agnet_name on tb_agent;
create index idx_agnet_name on tb_agent (ename(1))

此函数还对经理人的数据接口做了进一步的修改:若查询经理人的列表,通过 if self=action == list判断,若为真就只返回经理人的姓名、电话和服务星级这三个信息,否则会返回经理人的详细信息,可以用过api/agents/1/返回对应编号经理人的详细信息。

但是这里会遇到一个坑,经理人所对应的楼盘是一个多对多的关系,在数据库中用manytomany表示。在视图函数中需要通过prefetch_related来避免n+n查询问题,预抓取楼盘,并且只查询楼盘对应的名字,不需要楼盘的其他信息。

        if self.action == 'list':
            self.queryset = self.queryset.only('name', 'tel', 'servstar')
        else:
            # 避免1 + n查询
            # 一对一 / 多对一关联: select_related
            # 多对多关联: prefetch_related
            self.queryset = self.queryset.prefetch_related(
                Prefetch('estates',
                         queryset=Estate.objects.all().only('name').order_by('-hot'))
            )

这里还需要对AgentViewSet添加一个方法get_serializer_class,表示若查询经理人详情则通过AgentDetailSerializer来序列化字段,若查询经理人列表就通过AgentSimpleSerializer来学列话字段。

def get_serializer_class(self):
	if self.action in ('create', 'update'):
		return AgentCreateSerializer
	return AgentDetailSerializer if self.action == 'retrieve' else AgentSimpleSerializer

利用三方库做接口数据筛选

参考楼盘视图集的视图函数:

django-filter可以配合djangorestframework做接口数据筛选

首先需要在izufang/settings.py中加上配置文件

INSTALLED_APPS = [
    # 添加django-filter,这里加应用的时候,是“filters”而不是“filter”
    'django_filters',
]

在视图函数中添加

filter_backends = (DjangoFilterBackend, )
filter_fields = ('name', 'hot', 'district')

可以进行精确查询的,但是这样不够灵活。在djangi_filters中有一个DjangoFilterBackend类专门做接口数据筛选,而OrderingFilter是支持排序的一个类。通过django-filter可以做指定条件的筛选,但是都只能做精确筛选,我们可以自定义一个查询方法来实现模糊查询。在helper.py文件中创建一个EstateFilterSet自定义筛选器

class EstateFilterSet(filterset.FilterSet):
    """自定义楼盘筛选器"""
    # filter(name__contains='<name>')
    name = filterset.CharFilter(lookup_expr='contains')
    # filter(hot__gte=minhot, hot__lte=maxhot)
    minhot = filterset.NumberFilter(field_name='hot', lookup_expr='gte')
    maxhot = filterset.NumberFilter(field_name='hot', lookup_expr='lte')
    dist = filterset.NumberFilter(field_name='district')

    class Meta:
        model = Estate
        fields = ('name', 'minhot', 'maxhot', 'dist')

随后在视图函数中添加方法:

filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = EstateFilterSet
# 默认热度作为排序字段
ordering = '-hot'
# 其他排序可选字段
ordering_fields = ('district', 'hot', 'name')

在这里插入图片描述

【说明】

  1. 如果楼盘的名称需要做模糊查询,通过筛选字典的lookup_expr属性指定是精确还是模糊查询,可以使用exact/contains/startwith/endwith来表示方法。
  2. 如果需要根据楼盘热度范围来筛选,minhotmaxhot都对应了hot属性,但是需要用lookup_expr指定gt/gte/lt/lte
  3. 可以修改查询键的名称,可以通过field_name指定到原始的名称。

项目和项目源代码持续更新中,代码请参考我的码云:https://gitee.com/dcstempt_ping/izufang_rent
谢谢阅读❤

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代__-__代

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值