Chat
Search
Ithy Logo

Implementing Recharge Water Volume Statistics API in Django

Create easily a REST API with the django rest framework! - Jean-Loup ...

Introduction

In managing water resources, accurately tracking both usage and recharge statistics is crucial. This document outlines the implementation of a Django API endpoint designed to retrieve recharge water volume statistics, mirroring the existing api_device_water_usage_statistics function. The implementation emphasizes performance optimization, scalability, and maintainability.

Understanding the Models

DeviceInfoModel

The DeviceInfoModel represents information about each device, including identifiers, owner details, location, pricing, and various attributes related to water usage and monitoring.

  • device_id: Unique identifier for the device.
  • owner_name, owner_phone: Owner's contact information.
  • location: Foreign key linking to the Location model.
  • Other fields capturing device-specific attributes like pre_price, aft_price, and allowable_water_volume.

DeviceRechargeModel

The DeviceRechargeModel records recharge transactions for devices. It captures both monetary and water volume aspects of each recharge.

  • device_id, license_no: Identifiers linking the recharge to a specific device.
  • create_time: Timestamp of the recharge transaction.
  • current_recharge_money, total_recharge_money: Monetary values for the current and cumulative recharges.
  • current_recharge_water, total_recharge_water: Water volume for the current and cumulative recharges.
  • operate_user, note: Metadata about the operation.

DeviceDailyUsageModel

The DeviceDailyUsageModel tracks daily water and electricity usage for each device.

  • device_id: Identifier linking to the device.
  • statistics_date: Date of the usage record.
  • water_use, electricity_use: Daily usage metrics.
  • water_accumulation, electric_accumulation: Cumulative usage metrics.

API Requirements

The new API, api_device_recharge_statistics, aims to provide recharge water volume statistics with the following requirements:

  • Time-Based Filtering: Accepts search_time parameter to filter data by year or month.
  • Location-Based Filtering: Supports filtering by location hierarchy (location_l1_id, location_l2_id, location_l3_id).
  • Aggregation: Aggregates recharge water volume data either daily or monthly based on the statistics_type parameter.
  • Performance Optimization: Ensures efficient querying and data retrieval through indexing, selective data fetching, and caching.
  • Structured Response: Returns detailed recharge statistics for each device, including total recharge water and per-period usage.

Implementation Steps

1. Defining the API Endpoint

Create a new API function named api_device_recharge_statistics within your Django views. This function will handle GET requests to retrieve recharge statistics.

2. Handling Input Parameters

Extract and validate input parameters from the request:

  • search_time: Determines the time range for the query (e.g., "2024" for yearly, "2024-03" for monthly).
  • statistics_type: Specifies the aggregation type ("日" for daily, "月" for monthly).
  • location_l1_id, location_l2_id, location_l3_id: Filter devices based on location hierarchy.

3. Querying and Aggregating Data

Leverage Django's ORM capabilities to efficiently query and aggregate recharge data:

  • Filtering Devices: Apply location filters to narrow down the devices of interest.
  • Aggregating Recharge Data: Use annotate with aggregation functions like Sum to compute daily or monthly recharge volumes.
  • Optimizing Queries:
    • select_related and prefetch_related to reduce the number of database hits.
    • Use bulk_create and bulk_update for batch operations when necessary.
    • Implement indexing on frequently queried fields like license_no and device_id.

4. Formatting the Response

Structure the response to include comprehensive statistics for each device:

  • device_id, owner_name, license_no: Device metadata.
  • recharge_usage: List of recharge volumes per day or month.
  • total: Total recharge water volume.

5. Implementing Performance Optimizations

Incorporate strategies to enhance the API's performance:

  • Indexing: Ensure that database indexes are in place for fields used in filtering and lookups.
  • Caching: Utilize Django's caching framework to store frequently accessed data, reducing database load.
  • Pagination: Implement pagination to handle large datasets efficiently.
  • Selective Data Fetching: Use values() and values_list() to retrieve only necessary fields.

Code Implementation

API Function: api_device_recharge_statistics

The following Python code implements the api_device_recharge_statistics function, incorporating best practices for performance and maintainability:


@custom_require_http_methods(['GET'])
@json_request_handler
@token_validation_handler
def api_device_recharge_statistics(request):
    """
    Recharge Water Volume Statistics API
    """
    from django.db.models import Sum
    from django.db.models.functions import TruncDay, TruncMonth
    from django.core.cache import cache
    from django.core.paginator import Paginator
    import datetime
    from collections import defaultdict

    # Parse input parameters
    search_time = request.json_data.get('search_time')  # e.g., "2024" or "2024-03"
    statistics_type = request.json_data.get('statistics_type') or '日'  # Default to daily
    location_l1_id = request.json_data.get('location_l1_id')
    location_l2_id = request.json_data.get('location_l2_id')
    location_l3_id = request.json_data.get('location_l3_id')

    # Validate search_time
    if not search_time:
        return error_response('请指定查询时间')

    # Determine the date range based on search_time and statistics_type
    try:
        if statistics_type == '日':
            search_date = datetime.datetime.strptime(search_time, "%Y-%m")
            first_day = datetime.date(search_date.year, search_date.month, 1)
            if search_date.month < 12:
                last_day = datetime.date(search_date.year, search_date.month + 1, 1) - datetime.timedelta(days=1)
            else:
                last_day = datetime.date(search_date.year + 1, 1, 1) - datetime.timedelta(days=1)
        else:
            search_date = datetime.datetime.strptime(search_time, "%Y")
            first_day = datetime.date(search_date.year, 1, 1)
            last_day = datetime.date(search_date.year, 12, 31)
    except ValueError as e:
        return error_response(f'日期格式错误:{e}')

    # Filter devices based on location
    device_query = DeviceInfoModel.objects.all()
    if location_l3_id:
        device_query = device_query.filter(location_id=location_l3_id)
    elif location_l2_id:
        location = Location.objects.filter(id=location_l2_id).first()
        if location:
            device_query = device_query.filter(location__in=location.get_leafnodes())
    elif location_l1_id:
        location = Location.objects.filter(id=location_l1_id).first()
        if location:
            device_query = device_query.filter(location__in=location.get_leafnodes())

    if not device_query.exists():
        return error_response('未查询到设备数据')

    # Implement caching
    cache_key = f"recharge_stats_{statistics_type}_{search_time}_{location_l1_id}_{location_l2_id}_{location_l3_id}"
    cached_data = cache.get(cache_key)
    if cached_data:
        return success_response(data=cached_data)

    # Get license numbers for the devices
    license_numbers = list(device_query.values_list('license_no', flat=True))

    # Query recharge data
    if statistics_type == '日':
        recharge_data = DeviceRechargeModel.objects.filter(
            license_no__in=license_numbers,
            create_time__range=(first_day, last_day)
        ).annotate(
            day=TruncDay('create_time')
        ).values(
            'license_no', 'day'
        ).annotate(
            daily_recharge_water=Sum('current_recharge_water')
        )
    else:
        recharge_data = DeviceRechargeModel.objects.filter(
            license_no__in=license_numbers,
            create_time__range=(first_day, last_day)
        ).annotate(
            month=TruncMonth('create_time')
        ).values(
            'license_no', 'month'
        ).annotate(
            monthly_recharge_water=Sum('current_recharge_water')
        )

    # Group data by license_no
    data_by_license_no = defaultdict(dict)
    for entry in recharge_data:
        license_no = entry['license_no']
        if statistics_type == '日':
            date_key = entry['day'].date()
            recharge_water = entry['daily_recharge_water']
            data_by_license_no[license_no][date_key] = recharge_water
        else:
            date_key = entry['month'].date()
            recharge_water = entry['monthly_recharge_water']
            data_by_license_no[license_no][date_key] = recharge_water

    # Fetch device info in bulk to minimize queries
    device_info = DeviceInfoModel.objects.filter(license_no__in=license_numbers).values(
        'device_id', 'owner_name', 'license_no'
    )
    device_info_dict = {d['license_no']: d for d in device_info}

    # Prepare the response data
    specific = []
    for device in device_query:
        license_no = device.license_no
        recharge_usage = data_by_license_no.get(license_no, {})
        recharge_water_usage = {}
        total_recharge_water = 0

        if statistics_type == '日':
            date_range = (first_day + datetime.timedelta(days=i) for i in range((last_day - first_day).days + 1))
        else:
            date_range = (datetime.date(search_date.year, month, 1) for month in range(1, 13))

        for current_date in date_range:
            water_recharge = recharge_usage.get(current_date, 0)
            recharge_water_usage[current_date] = water_recharge
            total_recharge_water += water_recharge

        device_statistics_data = {
            "device_id": device.device_id,
            "owner_name": device.owner_name,
            "license_no": device.license_no,
            "recharge_usage": list(recharge_water_usage.values()),
            "total": total_recharge_water,
        }
        specific.append(device_statistics_data)

    # Optional: Implement pagination if the dataset is large
    paginator = Paginator(specific, 100)  # Show 100 devices per page
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    response_data = {
        "page": page_number,
        "total_pages": paginator.num_pages,
        "total_devices": paginator.count,
        "data": page_obj.object_list
    }

    # Cache the response data for 1 hour
    cache.set(cache_key, response_data, 60 * 60)

    return success_response(data=response_data)
    

Explanation of Key Components

Input Parameter Handling

The API accepts the following parameters:

  • search_time: Specifies the time frame for the query. It can be a year (e.g., "2024") or a year-month (e.g., "2024-03").
  • statistics_type: Determines the aggregation level. Defaults to daily ("日") if not specified.
  • location_l1_id, location_l2_id, location_l3_id: Used to filter devices based on their location hierarchy.

Date Range Calculation

Based on the search_time and statistics_type, the API calculates the appropriate date range:

  • Daily Statistics: For a given month, it determines the first and last days.
  • Monthly Statistics: For a given year, it spans from January 1st to December 31st.

Device Filtering

The API filters devices based on the provided location hierarchy. It uses the get_leafnodes() method to retrieve all leaf nodes under a specified location, ensuring comprehensive coverage.

Data Aggregation

Using Django ORM's aggregation functions:

  • TruncDay and TruncMonth: Truncate datetime fields to the desired granularity.
  • Sum: Aggregate the total recharge water volume per day or month.

Caching

To enhance performance, the API caches the response data based on the input parameters. This reduces database load for frequently requested data.

Pagination

For scenarios with a large number of devices, pagination is implemented to limit the number of records returned per request, ensuring efficient data handling and reduced response times.

Response Structure

The API response includes:

  • page: Current page number.
  • total_pages: Total number of pages available.
  • total_devices: Total number of devices matching the query.
  • data: List of recharge statistics for each device.

Each device's recharge statistics contain:

  • device_id, owner_name, license_no: Device metadata.
  • recharge_usage: Recharge volumes per period.
  • total: Total recharge water volume.

Performance Optimization Strategies

Indexing

Ensure that critical fields such as license_no and device_id are indexed in the database. This significantly improves query performance, especially for large datasets.


class DeviceInfoModel(models.Model):
    device_id = models.CharField(max_length=10, null=False, primary_key=True, db_index=True)
    license_no = models.CharField('许可证编号', max_length=255, null=True, db_index=True)
    # ... other fields ...
    
    class Meta:
        indexes = [
            models.Index(fields=['license_no']),
        ]
    

Batch Operations

When handling multiple records, use Django's bulk_create and bulk_update to minimize database queries.

Optimizing QuerySets

Utilize select_related and prefetch_related to optimize related object retrieval, thereby reducing the number of database hits.


# Example of using select_related for foreign keys
device_info = DeviceInfoModel.objects.select_related('location').get(device_id='device1')
    

Selective Data Fetching

Retrieve only necessary fields using values() or values_list() to reduce the amount of data transferred.


# Fetch only specific fields
recharge_data = DeviceRechargeModel.objects.filter(...)
    .values('license_no', 'create_time')
    .annotate(total_recharge=Sum('current_recharge_water'))
    

Caching

Implement caching for frequently accessed data to reduce database load and improve response times.

Pagination

Handle large datasets efficiently by breaking them into manageable pages, enhancing both performance and user experience.

Security Considerations

Ensure that the API is secure by implementing the following measures:

  • Input Validation: Validate all input parameters to prevent SQL injection and other malicious inputs.
  • Authentication and Authorization: Use token-based authentication to restrict access to authorized users.
  • Parameterization: When executing raw SQL queries, use parameterized queries to prevent SQL injection.

Testing and Validation

Rigorous testing ensures the reliability and accuracy of the API:

  • Unit Tests: Test individual components and functions to verify their behavior.
  • Integration Tests: Ensure that different parts of the system work together seamlessly.
  • Performance Testing: Assess the API's performance under various loads to identify potential bottlenecks.
  • Edge Cases: Test unusual or extreme scenarios to ensure the API handles them gracefully.

Conclusion

The api_device_recharge_statistics API provides a robust solution for retrieving recharge water volume statistics. By adhering to Django's best practices and implementing performance optimizations, the API ensures efficient data retrieval and scalability. Proper structuring and thorough testing further enhance its reliability, making it a valuable tool for managing water resource usage and recharge metrics.

References


Last updated January 6, 2025
Ask Ithy AI
Export Article
Delete Article