SQL查询以使用Django的模型将交替行合并为单个表。

huangapple go评论74阅读模式
英文:

SQL Query to combine alterative rows into a single table using Django's Models

问题

我已经创建了一个考勤服务器,允许员工记录他们的考勤。
有手动和自动两种记录考勤的方式。系统的SQL表格如下。

员工 本地用户 设备 生物识别记录 打卡记录
Id Id Id Id Id
姓名 姓名 地点 本地用户ID 员工ID
设备ID API密钥 打卡时间 打卡时间
员工ID 已禁用

每当添加了一个生物识别记录,如果该记录中的本地用户有一个员工外键,那么就会创建一个打卡记录。我觉得这有助于将自动和手动记录结合在一起。手动记录直接添加到打卡记录中

当用户请求考勤时,目前会获取活跃的打卡记录的交替行,并将其合并为一行。

输出数据格式

考勤记录
Id
打卡时间(上班)
打卡时间(下班)
员工ID

使用的代码

目前我正在使用原始的SQL查询来获取数据。

基本逻辑是:

  • 分成两个子查询 -> 上班打卡 & 下班打卡,用户唯一的Roll号
  • 与Roll号联接
    select i.id, username, i.punched_on as punch_in,  min(punch_out) as punch_out
    from (select *,row_number() over (partition by user_id order by user_id) as r_index from attendance_punch_log) as i
    left join local_user on i.user_id = local_user.id
    left join (
    select  o.logged_on as punch_out, username as out_user
    from (select *, row_number() over (partition by user_id order by user_id) as l_index from attendance_punch_log) as o
    left join local_user on o.user_id = local_user.id) as o on username = out_user and punch_out > punch_in
    where mod(r_index, 2) = 1
    group by username , r_index
    order by punch_in, punch_out

Django模型如下:

class BiometricsDevice(models.Model):
    # ... (此处省略部分代码)
    pass

class LocalUser(models.Model):
    # ... (此处省略部分代码)
    pass

class PunchLog(models.Model):
    # ... (此处省略部分代码)
    pass

class BiometricsLog(models.Model):
    # ... (此处省略部分代码)
    pass

用于创建查询集的代码如下:

def get(self, request):
    # ... (此处省略部分代码)
    pass

最后的想法

  • 在最后创建考勤表格时,我应该使用哪种其他方法?
  • 是创建一个新表格还是使用查询集更好?
英文:

I have created an attendance server that allows staffs to log their attendance.
There is a manual and a automatic way of logging attendance. The SQL tables for the system is as follows.

Staff Local User Device Biometrics Log Punch Log
Id Id Id Id Id
Name Name Location Local User ID Staff ID
Device ID API Key Punched On Punched On
Staff ID Is Disabled

Anytime a biometrics log is added, if the local user in that log has a staff foreign key then a punch log gets created. I felt that this helps in combining automatic and manual logs. Manual Logs are added straight to Punch Log

When the user is requesting for Attendance. This currently will take alterative rows of punch log that are active and combines it into a single row.

Output Data Format

Attendance Log
Id
Punch In
Punch Out
Staff ID

CODE IN USE

Currently I am using raw sql query to get the data

The basic logic is to

  • Split into two sub queries -> Punch In & Punch Out with Roll Number Unique to User
  • Join with Roll Number
    select i.id, username, i.punched_on as punch_in,  min(punch_out) as punch_out
    from (select *,row_number() over (partition by user_id order by user_id) as r_index from attendance_punch_log) as i
    left join local_user on i.user_id = local_user.id
    left join (
    select  o.logged_on as punch_out, username as out_user
    from (select *, row_number() over (partition by user_id order by user_id) as l_index from attendance_punch_log) as o
    left join local_user on o.user_id = local_user.id) as o on username = out_user and punch_out > punch_in
    where mod(r_index, 2) = 1
    group by username , r_index
    order by punch_in, punch_out

The Django models are as follows

class BiometricsDevice(models.Model):
    location = models.ForeignKey(
        Location, on_delete=models.CASCADE, related_name="device"
    )
    description = models.CharField(max_length=120)
    api_key = models.CharField(max_length=20, null=False, blank=False, unique=True)
    last_sync = models.DateTimeField(null=True)
    sync_user = models.BooleanField(default=True)
    is_active = models.BooleanField(default=True)

    created_on = models.DateField(auto_now_add=True)
    created_by = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name="created_biometrics",
    )

    def __str__(self):
        return f"{self.location.name} - {self.api_key}"


class LocalUser(models.Model):
    ref = models.PositiveIntegerField()
    name = models.CharField(max_length=20)
    device = models.ForeignKey(
        BiometricsDevice, on_delete=models.CASCADE, related_name="users"
    )
    # Signal on staff update -> Re-Run Biometrics Log to Punch Log Conversion
    staff = models.ForeignKey(
        Staff,
        on_delete=models.CASCADE,
        related_name="biometric_user",
        blank=True,
        null=True,
    )

    def __str__(self):
        return f"{self.name} - {self.device}"

    class Meta:
        unique_together = (["name", "device"], ["name", "ref", "device"])


class PunchLog(models.Model):
    staff = models.ForeignKey(
        Staff,
        on_delete=models.CASCADE,
        related_name="punch_log",
    )
    punched_on = models.DateTimeField()
    is_active = models.BooleanField(default=True)

    created_on = models.DateField(auto_now_add=True)
    created_by = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name="created_punch_logs",
        null=True,
        blank=True,
    )

    def __str__(self):
        return f"{self.staff.name} - {self.punched_on}"


class BiometricsLog(models.Model):
    user = models.ForeignKey(
        LocalUser,
        on_delete=models.CASCADE,
        related_name="biometrics_log",
    )
    punched_on = models.DateTimeField()

    punch_log = models.OneToOneField(
        PunchLog,
        on_delete=models.CASCADE,
        related_name="biometrics_log",
        null=True,
        blank=True,
    )

    class Meta:
        unique_together = ["user", "punched_on"]

    def __str__(self):
        return f"{self.user.name} - {self.punched_on}"

The Code to create queryset is as follows

    def get(self, request):
        punch_in = (
            BiometricsLog.objects.annotate(
                index=Window(
                    expression=RowNumber(),
                    order_by=F("user_id").asc(),
                    partition_by=[F("user_id")],
                ),
            )
            .annotate(evenindex=F("index") % 2)
            .filter(evenindex=1)
            .order_by("-punched_on")
        )
        punch_out = (
            BiometricsLog.objects.annotate(
                index=Window(
                    expression=RowNumber(),
                    order_by=F("user_id").asc(),
                    partition_by=[F("user_id")],
                ),
            )
            .annotate(evenindex=F("index") % 2)
            .filter(evenindex=0)
            .order_by("-punched_on")
        )
        # Odd Even index join

        queryset = punch_in.annotate(
            punch_in=F("punched_on"),
            punch_out=Subquery(
                punch_out.filter(
                    index__gt=OuterRef("index"),
                    user_id=OuterRef("user_id"),
                ).values("punched_on")[:1]
            ),
            staff=F("user__staff"),
        )

Final Thoughts

  • What other method should i be using to create attendance table in the end ?
  • Is it better to create a new table insted of a querset ?

答案1

得分: 0

似乎对我来说,在打卡日志中确定“in”或“out”的方法过于复杂。这确实会有维护问题,也可能影响性能。

你可以简单地向打卡日志模型添加一个“direction”字段(甚至只是布尔值)。这将使查询和其他一切都更简单和更可读。

class PunchLog(models.Model):
...
is_in = models.BooleanField(default=True)

对另一个日志模型进行类似调整可能也是有意义的。

编辑

然而,也许你想要审查整个模型架构。我认为这两个日志非常相似,实际上可以视为同一事物。而且,在只发生一个事件时,在某些不明显的情况下制作两个日志条目在概念上是不正确的。使用更简单的架构,查询变得微不足道。

将你的日志合并怎么样?

class LogEntry(models.Model):
local_user = models.ForeignKey(...)
created_on = models.DateTimeField(auto_now_add=True)
is_in = models.BooleanField(...)
is_biometric = models.BooleanField(...)
英文:

It seems to me that the method of determining "in" or "out" in the punch log is overly complicated. This certainly has maintenance issues and could also affect performance.

You could simply add a direction field (or even just a boolean) to your punch log model. It would make the queries and everything else much simpler and more readable.

class PunchLog(models.Model):
...
is_in = models.BooleanField(default=True)

It might make sense make a similar adjustment to the other log model as well.

EDIT

However, perhaps you want to review your entire model architecture. I think that both logs are similar enough to be the practically the same thing. Also, I think it is conceptually not correct to make two log entries in certain not obvious conditions, when only one event occurs. With a simpler architecture, the queries become trivial.

How about combining your logs?

class LogEntry(models.Model):
local_user = models.ForeignKey(...)
created_on = models.DateTimeField(auto_now_add=True)
is_in = models.BooleanField(...)
is_biometric = models.BooleanField(...)

huangapple
  • 本文由 发表于 2023年6月15日 19:58:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76482232.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定